From 033d2bc52469ff658757f0eb762ed0e52fcc6d28 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Wed, 16 Dec 2020 09:54:09 -0600 Subject: [PATCH 01/69] Added initial static_reduction_map files. Copied existing static_map files and just renamed all references to static_map to static_reduction_map. --- include/cuco/detail/static_reduction_map.inl | 396 ++++++++ .../detail/static_reduction_map_kernels.cuh | 386 ++++++++ include/cuco/static_reduction_map.cuh | 929 ++++++++++++++++++ tests/CMakeLists.txt | 12 +- .../static_reduction_map_test.cu | 355 +++++++ 5 files changed, 2070 insertions(+), 8 deletions(-) create mode 100644 include/cuco/detail/static_reduction_map.inl create mode 100644 include/cuco/detail/static_reduction_map_kernels.cuh create mode 100644 include/cuco/static_reduction_map.cuh create mode 100644 tests/static_reduction_map/static_reduction_map_test.cu diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl new file mode 100644 index 000000000..243032f6b --- /dev/null +++ b/include/cuco/detail/static_reduction_map.inl @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2020, 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. + */ + +namespace cuco { + +/**---------------------------------------------------------------------------* + * @brief Enumeration of the possible results of attempting to insert into + *a hash bucket + *---------------------------------------------------------------------------**/ +enum class insert_result { + CONTINUE, ///< Insert did not succeed, continue trying to insert + SUCCESS, ///< New pair inserted successfully + DUPLICATE ///< Insert did not succeed, key is already present +}; + +template +static_reduction_map::static_reduction_map(std::size_t capacity, + Key empty_key_sentinel, + Value empty_value_sentinel, + Allocator const& alloc) + : capacity_{capacity}, + empty_key_sentinel_{empty_key_sentinel}, + empty_value_sentinel_{empty_value_sentinel}, + slot_allocator_{alloc} +{ + slots_ = std::allocator_traits::allocate(slot_allocator_, capacity); + + auto constexpr block_size = 256; + auto constexpr stride = 4; + auto const grid_size = (capacity + stride * block_size - 1) / (stride * block_size); + detail::initialize + <<>>(slots_, empty_key_sentinel, empty_value_sentinel, capacity); + + CUCO_CUDA_TRY(cudaMallocManaged(&num_successes_, sizeof(atomic_ctr_type))); +} + +template +static_reduction_map::~static_reduction_map() +{ + std::allocator_traits::deallocate(slot_allocator_, slots_, capacity_); + CUCO_CUDA_TRY(cudaFree(num_successes_)); +} + +template +template +void static_reduction_map::insert(InputIt first, + InputIt last, + Hash hash, + KeyEqual key_equal) +{ + auto num_keys = std::distance(first, last); + auto const block_size = 128; + auto const stride = 1; + auto const tile_size = 4; + auto const grid_size = (tile_size * num_keys + stride * block_size - 1) / (stride * block_size); + auto view = get_device_mutable_view(); + + *num_successes_ = 0; + int device_id; + CUCO_CUDA_TRY(cudaGetDevice(&device_id)); + CUCO_CUDA_TRY(cudaMemPrefetchAsync(num_successes_, sizeof(atomic_ctr_type), device_id)); + + detail::insert + <<>>(first, first + num_keys, num_successes_, view, hash, key_equal); + CUCO_CUDA_TRY(cudaDeviceSynchronize()); + + size_ += num_successes_->load(cuda::std::memory_order_relaxed); +} + +template +template +void static_reduction_map::find( + InputIt first, InputIt last, OutputIt output_begin, Hash hash, KeyEqual key_equal) noexcept +{ + auto num_keys = std::distance(first, last); + auto const block_size = 128; + auto const stride = 1; + auto const tile_size = 4; + auto const grid_size = (tile_size * num_keys + stride * block_size - 1) / (stride * block_size); + auto view = get_device_view(); + + detail::find + <<>>(first, last, output_begin, view, hash, key_equal); + CUCO_CUDA_TRY(cudaDeviceSynchronize()); +} + +template +template +void static_reduction_map::contains( + InputIt first, InputIt last, OutputIt output_begin, Hash hash, KeyEqual key_equal) noexcept +{ + auto num_keys = std::distance(first, last); + auto const block_size = 128; + auto const stride = 1; + auto const tile_size = 4; + auto const grid_size = (tile_size * num_keys + stride * block_size - 1) / (stride * block_size); + auto view = get_device_view(); + + detail::contains + <<>>(first, last, output_begin, view, hash, key_equal); + CUCO_CUDA_TRY(cudaDeviceSynchronize()); +} + +template +template +__device__ bool static_reduction_map::device_mutable_view::insert( + value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept +{ + auto current_slot{initial_slot(insert_pair.first, hash)}; + + while (true) { + using cuda::std::memory_order_relaxed; + auto expected_key = this->get_empty_key_sentinel(); + auto expected_value = this->get_empty_value_sentinel(); + auto& slot_key = current_slot->first; + auto& slot_value = current_slot->second; + + bool key_success = + slot_key.compare_exchange_strong(expected_key, insert_pair.first, memory_order_relaxed); + bool value_success = + slot_value.compare_exchange_strong(expected_value, insert_pair.second, memory_order_relaxed); + + if (key_success) { + while (not value_success) { + value_success = + slot_value.compare_exchange_strong(expected_value = this->get_empty_value_sentinel(), + insert_pair.second, + memory_order_relaxed); + } + return true; + } else if (value_success) { + slot_value.store(this->get_empty_value_sentinel(), memory_order_relaxed); + } + + // if the key was already inserted by another thread, than this instance is a + // duplicate, so the insert fails + if (key_equal(insert_pair.first, expected_key)) { return false; } + + // if we couldn't insert the key, but it wasn't a duplicate, then there must + // have been some other key there, so we keep looking for a slot + current_slot = next_slot(current_slot); + } +} + +template +template +__device__ bool static_reduction_map::device_mutable_view::insert( + CG g, value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept +{ + auto current_slot = initial_slot(g, insert_pair.first, hash); + + while (true) { + key_type const existing_key = current_slot->first; + + // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as the + // sentinel is not a valid key value. Therefore, first check for the sentinel + auto const slot_is_empty = (existing_key == this->get_empty_key_sentinel()); + + // the key we are trying to insert is already in the map, so we return with failure to insert + if (g.ballot(not slot_is_empty and key_equal(existing_key, insert_pair.first))) { + return false; + } + + auto const window_contains_empty = g.ballot(slot_is_empty); + + // we found an empty slot, but not the key we are inserting, so this must + // be an empty slot into which we can insert the key + if (window_contains_empty) { + // the first lane in the group with an empty slot will attempt the insert + insert_result status{insert_result::CONTINUE}; + uint32_t src_lane = __ffs(window_contains_empty) - 1; + + if (g.thread_rank() == src_lane) { + using cuda::std::memory_order_relaxed; + auto expected_key = this->get_empty_key_sentinel(); + auto expected_value = this->get_empty_value_sentinel(); + auto& slot_key = current_slot->first; + auto& slot_value = current_slot->second; + + bool key_success = + slot_key.compare_exchange_strong(expected_key, insert_pair.first, memory_order_relaxed); + bool value_success = slot_value.compare_exchange_strong( + expected_value, insert_pair.second, memory_order_relaxed); + + if (key_success) { + while (not value_success) { + value_success = + slot_value.compare_exchange_strong(expected_value = this->get_empty_value_sentinel(), + insert_pair.second, + memory_order_relaxed); + } + status = insert_result::SUCCESS; + } else if (value_success) { + slot_value.store(this->get_empty_value_sentinel(), memory_order_relaxed); + } + + // our key was already present in the slot, so our key is a duplicate + if (key_equal(insert_pair.first, expected_key)) { status = insert_result::DUPLICATE; } + // another key was inserted in the slot we wanted to try + // so we need to try the next empty slot in the window + } + + uint32_t res_status = g.shfl(static_cast(status), src_lane); + status = static_cast(res_status); + + // successful insert + if (status == insert_result::SUCCESS) { return true; } + // duplicate present during insert + if (status == insert_result::DUPLICATE) { return false; } + // if we've gotten this far, a different key took our spot + // before we could insert. We need to retry the insert on the + // same window + } + // if there are no empty slots in the current window, + // we move onto the next window + else { + current_slot = next_slot(g, current_slot); + } + } +} + +template +template +__device__ typename static_reduction_map::device_view::iterator +static_reduction_map::device_view::find(Key const& k, + Hash hash, + KeyEqual key_equal) noexcept +{ + auto current_slot = initial_slot(k, hash); + + while (true) { + auto const existing_key = current_slot->first.load(cuda::std::memory_order_relaxed); + // Key doesn't exist, return end() + if (existing_key == this->get_empty_key_sentinel()) { return this->end(); } + + // Key exists, return iterator to location + if (key_equal(existing_key, k)) { return current_slot; } + + current_slot = next_slot(current_slot); + } +} + +template +template +__device__ typename static_reduction_map::device_view::const_iterator +static_reduction_map::device_view::find(Key const& k, + Hash hash, + KeyEqual key_equal) const + noexcept +{ + auto current_slot = initial_slot(k, hash); + + while (true) { + auto const existing_key = current_slot->first.load(cuda::std::memory_order_relaxed); + // Key doesn't exist, return end() + if (existing_key == this->get_empty_key_sentinel()) { return this->end(); } + + // Key exists, return iterator to location + if (key_equal(existing_key, k)) { return current_slot; } + + current_slot = next_slot(current_slot); + } +} + +template +template +__device__ typename static_reduction_map::device_view::iterator +static_reduction_map::device_view::find(CG g, + Key const& k, + Hash hash, + KeyEqual key_equal) noexcept +{ + auto current_slot = initial_slot(g, k, hash); + + while (true) { + auto const existing_key = current_slot->first.load(cuda::std::memory_order_relaxed); + + // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as the + // sentinel is not a valid key value. Therefore, first check for the sentinel + auto const slot_is_empty = (existing_key == this->get_empty_key_sentinel()); + + // the key we were searching for was found by one of the threads, + // so we return an iterator to the entry + auto const exists = g.ballot(not slot_is_empty and key_equal(existing_key, k)); + if (exists) { + uint32_t src_lane = __ffs(exists) - 1; + // TODO: This shouldn't cast an iterator to an int to shuffle. Instead, get the index of the + // current_slot and shuffle that instead. + intptr_t res_slot = g.shfl(reinterpret_cast(current_slot), src_lane); + return reinterpret_cast(res_slot); + } + + // we found an empty slot, meaning that the key we're searching for isn't present + if (g.ballot(slot_is_empty)) { return this->end(); } + + // otherwise, all slots in the current window are full with other keys, so we move onto the + // next window + current_slot = next_slot(g, current_slot); + } +} + +template +template +__device__ typename static_reduction_map::device_view::const_iterator +static_reduction_map::device_view::find( + CG g, Key const& k, Hash hash, KeyEqual key_equal) const noexcept +{ + auto current_slot = initial_slot(g, k, hash); + + while (true) { + auto const existing_key = current_slot->first.load(cuda::std::memory_order_relaxed); + + // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as the + // sentinel is not a valid key value. Therefore, first check for the sentinel + auto const slot_is_empty = (existing_key == this->get_empty_key_sentinel()); + + // the key we were searching for was found by one of the threads, so we return an iterator to + // the entry + auto const exists = g.ballot(not slot_is_empty and key_equal(existing_key, k)); + if (exists) { + uint32_t src_lane = __ffs(exists) - 1; + // TODO: This shouldn't cast an iterator to an int to shuffle. Instead, get the index of the + // current_slot and shuffle that instead. + intptr_t res_slot = g.shfl(reinterpret_cast(current_slot), src_lane); + return reinterpret_cast(res_slot); + } + + // we found an empty slot, meaning that the key we're searching + // for isn't in this submap, so we should move onto the next one + if (g.ballot(slot_is_empty)) { return this->end(); } + + // otherwise, all slots in the current window are full with other keys, + // so we move onto the next window in the current submap + + current_slot = next_slot(g, current_slot); + } +} + +template +template +__device__ bool static_reduction_map::device_view::contains( + Key const& k, Hash hash, KeyEqual key_equal) noexcept +{ + auto current_slot = initial_slot(k, hash); + + while (true) { + auto const existing_key = current_slot->first.load(cuda::std::memory_order_relaxed); + + if (existing_key == empty_key_sentinel_) { return false; } + + if (key_equal(existing_key, k)) { return true; } + + current_slot = next_slot(current_slot); + } +} + +template +template +__device__ bool static_reduction_map::device_view::contains( + CG g, Key const& k, Hash hash, KeyEqual key_equal) noexcept +{ + auto current_slot = initial_slot(g, k, hash); + + while (true) { + key_type const existing_key = current_slot->first.load(cuda::std::memory_order_relaxed); + + // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as the + // sentinel is not a valid key value. Therefore, first check for the sentinel + auto const slot_is_empty = (existing_key == this->get_empty_key_sentinel()); + + // the key we were searching for was found by one of the threads, so we return an iterator to + // the entry + if (g.ballot(not slot_is_empty and key_equal(existing_key, k))) { return true; } + + // we found an empty slot, meaning that the key we're searching for isn't present + if (g.ballot(slot_is_empty)) { return false; } + + // otherwise, all slots in the current window are full with other keys, so we move onto the next + // window + current_slot = next_slot(g, current_slot); + } +} +} // namespace cuco diff --git a/include/cuco/detail/static_reduction_map_kernels.cuh b/include/cuco/detail/static_reduction_map_kernels.cuh new file mode 100644 index 000000000..6ded5e99d --- /dev/null +++ b/include/cuco/detail/static_reduction_map_kernels.cuh @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2020, 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. + */ + +namespace cuco { +namespace detail { +namespace cg = cooperative_groups; + +/** + * @brief Initializes each slot in the flat `slots` storage to contain `k` and `v`. + * + * Each space in `slots` that can hold a key value pair is initialized to a + * `pair_atomic_type` containing the key `k` and the value `v`. + * + * @tparam atomic_key_type Type of the `Key` atomic container + * @tparam atomic_mapped_type Type of the `Value` atomic container + * @tparam Key key type + * @tparam Value value type + * @tparam pair_atomic_type key/value pair type + * @param slots Pointer to flat storage for the map's key/value pairs + * @param k Key to which all keys in `slots` are initialized + * @param v Value to which all values in `slots` are initialized + * @param size Size of the storage pointed to by `slots` + */ +template +__global__ void initialize(pair_atomic_type* const slots, Key k, Value v, std::size_t size) +{ + auto tid = threadIdx.x + blockIdx.x * blockDim.x; + while (tid < size) { + new (&slots[tid].first) atomic_key_type{k}; + new (&slots[tid].second) atomic_mapped_type{v}; + tid += gridDim.x * blockDim.x; + } +} + +/** + * @brief Inserts all key/value pairs in the range `[first, last)`. + * + * If multiple keys in `[first, last)` compare equal, it is unspecified which + * element is inserted. + * + * @tparam block_size + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the map's `value_type` + * @tparam atomicT Type of atomic storage + * @tparam viewT Type of device view allowing access of hash map storage + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of key/value pairs + * @param last End of the sequence of key/value pairs + * @param num_successes The number of successfully inserted key/value pairs + * @param view Mutable device view used to access the hash map's slot storage + * @param hash The unary function to apply to hash each key + * @param key_equal The binary function used to compare two keys for equality + */ +template +__global__ void insert( + InputIt first, InputIt last, atomicT* num_successes, viewT view, Hash hash, KeyEqual key_equal) +{ + typedef cub::BlockReduce BlockReduce; + __shared__ typename BlockReduce::TempStorage temp_storage; + std::size_t thread_num_successes = 0; + + auto tid = blockDim.x * blockIdx.x + threadIdx.x; + auto it = first + tid; + + while (it < last) { + typename viewT::value_type const insert_pair{*it}; + if (view.insert(insert_pair, hash, key_equal)) { thread_num_successes++; } + it += gridDim.x * blockDim.x; + } + + // compute number of successfully inserted elements for each block + // and atomically add to the grand total + std::size_t block_num_successes = BlockReduce(temp_storage).Sum(thread_num_successes); + if (threadIdx.x == 0) { *num_successes += block_num_successes; } +} + +/** + * @brief Inserts all key/value pairs in the range `[first, last)`. + * + * If multiple keys in `[first, last)` compare equal, it is unspecified which + * element is inserted. Uses the CUDA Cooperative Groups API to leverage groups + * of multiple threads to perform each key/value insertion. This provides a + * significant boost in throughput compared to the non Cooperative Group + * `insert` at moderate to high load factors. + * + * @tparam block_size + * @tparam tile_size The number of threads in the Cooperative Groups used to perform + * inserts + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the map's `value_type` + * @tparam atomicT Type of atomic storage + * @tparam viewT Type of device view allowing access of hash map storage + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of key/value pairs + * @param last End of the sequence of key/value pairs + * @param num_successes The number of successfully inserted key/value pairs + * @param view Mutable device view used to access the hash map's slot storage + * @param hash The unary function to apply to hash each key + * @param key_equal The binary function used to compare two keys for equality + */ +template +__global__ void insert( + InputIt first, InputIt last, atomicT* num_successes, viewT view, Hash hash, KeyEqual key_equal) +{ + typedef cub::BlockReduce BlockReduce; + __shared__ typename BlockReduce::TempStorage temp_storage; + std::size_t thread_num_successes = 0; + + auto tile = cg::tiled_partition(cg::this_thread_block()); + auto tid = blockDim.x * blockIdx.x + threadIdx.x; + auto it = first + tid / tile_size; + + while (it < last) { + // force conversion to value_type + typename viewT::value_type const insert_pair{*it}; + if (view.insert(tile, insert_pair, hash, key_equal) && tile.thread_rank() == 0) { + thread_num_successes++; + } + it += (gridDim.x * blockDim.x) / tile_size; + } + + // compute number of successfully inserted elements for each block + // and atomically add to the grand total + std::size_t block_num_successes = BlockReduce(temp_storage).Sum(thread_num_successes); + if (threadIdx.x == 0) { *num_successes += block_num_successes; } +} + +/** + * @brief Finds the values corresponding to all keys in the range `[first, last)`. + * + * If the key `*(first + i)` exists in the map, copies its associated value to `(output_begin + i)`. + * Else, copies the empty value sentinel. + * @tparam block_size The size of the thread block + * @tparam Value The type of the mapped value for the map + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the map's `key_type` + * @tparam OutputIt Device accessible output iterator whose `value_type` is + * convertible to the map's `mapped_type` + * @tparam viewT Type of device view allowing access of hash map storage + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param output_begin Beginning of the sequence of values retrieved for each key + * @param view Device view used to access the hash map's slot storage + * @param hash The unary function to apply to hash each key + * @param key_equal The binary function to compare two keys for equality + */ +template +__global__ void find( + InputIt first, InputIt last, OutputIt output_begin, viewT view, Hash hash, KeyEqual key_equal) +{ + auto tid = blockDim.x * blockIdx.x + threadIdx.x; + auto key_idx = tid; + __shared__ Value writeBuffer[block_size]; + + while (first + key_idx < last) { + auto key = *(first + key_idx); + auto found = view.find(key, hash, key_equal); + + /* + * The ld.relaxed.gpu instruction used in view.find causes L1 to + * flush more frequently, causing increased sector stores from L2 to global memory. + * By writing results to shared memory and then synchronizing before writing back + * to global, we no longer rely on L1, preventing the increase in sector stores from + * L2 to global and improving performance. + */ + writeBuffer[threadIdx.x] = found->second.load(cuda::std::memory_order_relaxed); + __syncthreads(); + *(output_begin + key_idx) = writeBuffer[threadIdx.x]; + key_idx += gridDim.x * blockDim.x; + } +} + +/** + * @brief Finds the values corresponding to all keys in the range `[first, last)`. + * + * If the key `*(first + i)` exists in the map, copies its associated value to `(output_begin + i)`. + * Else, copies the empty value sentinel. Uses the CUDA Cooperative Groups API to leverage groups + * of multiple threads to find each key. This provides a significant boost in throughput compared + * to the non Cooperative Group `find` at moderate to high load factors. + * + * @tparam block_size The size of the thread block + * @tparam tile_size The number of threads in the Cooperative Groups used to perform + * inserts + * @tparam Value The type of the mapped value for the map + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the map's `key_type` + * @tparam OutputIt Device accessible output iterator whose `value_type` is + * convertible to the map's `mapped_type` + * @tparam viewT Type of device view allowing access of hash map storage + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param output_begin Beginning of the sequence of values retrieved for each key + * @param view Device view used to access the hash map's slot storage + * @param hash The unary function to apply to hash each key + * @param key_equal The binary function to compare two keys for equality + */ +template +__global__ void find( + InputIt first, InputIt last, OutputIt output_begin, viewT view, Hash hash, KeyEqual key_equal) +{ + auto tile = cg::tiled_partition(cg::this_thread_block()); + auto tid = blockDim.x * blockIdx.x + threadIdx.x; + auto key_idx = tid / tile_size; + __shared__ Value writeBuffer[block_size]; + + while (first + key_idx < last) { + auto key = *(first + key_idx); + auto found = view.find(tile, key, hash, key_equal); + + /* + * The ld.relaxed.gpu instruction used in view.find causes L1 to + * flush more frequently, causing increased sector stores from L2 to global memory. + * By writing results to shared memory and then synchronizing before writing back + * to global, we no longer rely on L1, preventing the increase in sector stores from + * L2 to global and improving performance. + */ + if (tile.thread_rank() == 0) { + writeBuffer[threadIdx.x / tile_size] = found->second.load(cuda::std::memory_order_relaxed); + } + __syncthreads(); + if (tile.thread_rank() == 0) { + *(output_begin + key_idx) = writeBuffer[threadIdx.x / tile_size]; + } + key_idx += (gridDim.x * blockDim.x) / tile_size; + } +} + +/** + * @brief Indicates whether the keys in the range `[first, last)` are contained in the map. + * + * Writes a `bool` to `(output + i)` indicating if the key `*(first + i)` exists in the map. + * + * @tparam block_size The size of the thread block + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the map's `key_type` + * @tparam OutputIt Device accessible output iterator whose `value_type` is + * convertible to the map's `mapped_type` + * @tparam viewT Type of device view allowing access of hash map storage + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param output_begin Beginning of the sequence of booleans for the presence of each key + * @param view Device view used to access the hash map's slot storage + * @param hash The unary function to apply to hash each key + * @param key_equal The binary function to compare two keys for equality + */ +template +__global__ void contains( + InputIt first, InputIt last, OutputIt output_begin, viewT view, Hash hash, KeyEqual key_equal) +{ + auto tid = blockDim.x * blockIdx.x + threadIdx.x; + auto key_idx = tid; + __shared__ bool writeBuffer[block_size]; + + while (first + key_idx < last) { + auto key = *(first + key_idx); + + /* + * The ld.relaxed.gpu instruction used in view.find causes L1 to + * flush more frequently, causing increased sector stores from L2 to global memory. + * By writing results to shared memory and then synchronizing before writing back + * to global, we no longer rely on L1, preventing the increase in sector stores from + * L2 to global and improving performance. + */ + writeBuffer[threadIdx.x] = view.contains(key, hash, key_equal); + __syncthreads(); + *(output_begin + key_idx) = writeBuffer[threadIdx.x]; + key_idx += gridDim.x * blockDim.x; + } +} + +/** + * @brief Indicates whether the keys in the range `[first, last)` are contained in the map. + * + * Writes a `bool` to `(output + i)` indicating if the key `*(first + i)` exists in the map. + * Uses the CUDA Cooperative Groups API to leverage groups of multiple threads to perform the + * contains operation for each key. This provides a significant boost in throughput compared + * to the non Cooperative Group `contains` at moderate to high load factors. + * + * @tparam block_size The size of the thread block + * @tparam tile_size The number of threads in the Cooperative Groups used to perform + * inserts + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the map's `key_type` + * @tparam OutputIt Device accessible output iterator whose `value_type` is + * convertible to the map's `mapped_type` + * @tparam viewT Type of device view allowing access of hash map storage + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param output_begin Beginning of the sequence of booleans for the presence of each key + * @param view Device view used to access the hash map's slot storage + * @param hash The unary function to apply to hash each key + * @param key_equal The binary function to compare two keys for equality + */ +template +__global__ void contains( + InputIt first, InputIt last, OutputIt output_begin, viewT view, Hash hash, KeyEqual key_equal) +{ + auto tile = cg::tiled_partition(cg::this_thread_block()); + auto tid = blockDim.x * blockIdx.x + threadIdx.x; + auto key_idx = tid / tile_size; + __shared__ bool writeBuffer[block_size]; + + while (first + key_idx < last) { + auto key = *(first + key_idx); + auto found = view.contains(tile, key, hash, key_equal); + + /* + * The ld.relaxed.gpu instruction used in view.find causes L1 to + * flush more frequently, causing increased sector stores from L2 to global memory. + * By writing results to shared memory and then synchronizing before writing back + * to global, we no longer rely on L1, preventing the increase in sector stores from + * L2 to global and improving performance. + */ + if (tile.thread_rank() == 0) { writeBuffer[threadIdx.x / tile_size] = found; } + __syncthreads(); + if (tile.thread_rank() == 0) { + *(output_begin + key_idx) = writeBuffer[threadIdx.x / tile_size]; + } + key_idx += (gridDim.x * blockDim.x) / tile_size; + } +} + +} // namespace detail +} // namespace cuco \ No newline at end of file diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh new file mode 100644 index 000000000..241ef480d --- /dev/null +++ b/include/cuco/static_reduction_map.cuh @@ -0,0 +1,929 @@ +/* + * Copyright (c) 2020, 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 +#include +#include + +#include +#include +#ifndef CUDART_VERSION +#error CUDART_VERSION Undefined! +#elif (CUDART_VERSION >= 11000) // including with CUDA 10.2 leads to compilation errors +#include +#endif + +#include +#include +#include +#include + +namespace cuco { + +/** + * @brief Possible reduction operations that can be performed by a `static_reduction_map`. + * + * `GENERIC` allows for any associative binary reduction operation, but may have worse performance + * compared to one of the native operations. + * + */ +enum class reduction_op { + SUM, ///< Addition + SUB, ///< Subtraction + MIN, ///< Minimum value + MAX, ///< Maximum value + AND, ///< Bitwise AND + OR, ///< Bitwise OR + XOR, ///< Bitwise XOR + GENERIC ///< User-defined, associative binary operation +}; + +/** + * @brief A GPU-accelerated, unordered, associative container of key-value + * pairs with unique keys. + * + * Allows constant time concurrent inserts or concurrent find operations (not + * concurrent insert and find) from threads in device code. + * + * Current limitations: + * - Requires keys that are Arithmetic + * - Does not support erasing keys + * - Capacity is fixed and will not grow automatically + * - Requires the user to specify sentinel values for both key and mapped value + * to indicate empty slots + * - Does not support concurrent insert and find operations + * + * The `static_reduction_map` supports two types of operations: + * - Host-side "bulk" operations + * - Device-side "singular" operations + * + * The host-side bulk operations include `insert`, `find`, and `contains`. These + * APIs should be used when there are a large number of keys to insert or lookup + * in the map. For example, given a range of keys specified by device-accessible + * iterators, the bulk `insert` function will insert all keys into the map. + * + * The singular device-side operations allow individual threads to to perform + * independent insert or find/contains operations from device code. These + * operations are accessed through non-owning, trivially copyable "view" types: + * `device_view` and `mutable_device_view`. The `device_view` class is an + * immutable view that allows only non-modifying operations such as `find` or + * `contains`. The `mutable_device_view` class only allows `insert` operations. + * The two types are separate to prevent erroneous concurrent insert/find + * operations. + * + * Example: + * \code{.cpp} + * int empty_key_sentinel = -1; + * int empty_value_sentine = -1; + * + * // Constructs a map with 100,000 slots using -1 and -1 as the empty key/value + * // sentinels. Note the capacity is chosen knowing we will insert 50,000 keys, + * // for an load factor of 50%. + * static_reduction_map m{100'000, empty_key_sentinel, empty_value_sentinel}; + * + * // Create a sequence of pairs {{0,0}, {1,1}, ... {i,i}} + * thrust::device_vector> pairs(50,000); + * thrust::transform(thrust::make_counting_iterator(0), + * thrust::make_counting_iterator(pairs.size()), + * pairs.begin(), + * []__device__(auto i){ return thrust::make_pair(i,i); }; + * + * + * // Inserts all pairs into the map + * m.insert(pairs.begin(), pairs.end()); + * + * // Get a `device_view` and passes it to a kernel where threads may perform + * // `find/contains` lookups + * kernel<<<...>>>(m.get_device_view()); + * \endcode + * + * + * @tparam Key Arithmetic type used for key + * @tparam Value Type of the mapped values + * @tparam Scope The scope in which insert/find operations will be performed by + * individual threads. + * @tparam Allocator Type of allocator used for device storage + */ +template > +class static_reduction_map { + static_assert(std::is_arithmetic::value, "Unsupported, non-arithmetic key type."); + + public: + using value_type = cuco::pair_type; + using key_type = Key; + using mapped_type = Value; + using atomic_key_type = cuda::atomic; + using atomic_mapped_type = cuda::atomic; + using pair_atomic_type = cuco::pair_type; + using atomic_ctr_type = cuda::atomic; + using allocator_type = Allocator; + using slot_allocator_type = + typename std::allocator_traits::rebind_alloc; + + static_reduction_map(static_reduction_map const&) = delete; + static_reduction_map(static_reduction_map&&) = delete; + static_reduction_map& operator=(static_reduction_map const&) = delete; + static_reduction_map& operator=(static_reduction_map&&) = delete; + + /** + * @brief Construct a fixed-size map with the specified capacity and sentinel values. + * @brief Construct a statically sized map with the specified number of slots + * and sentinel values. + * + * The capacity of the map is fixed. Insert operations will not automatically + * grow the map. Attempting to insert more unique keys than the capacity of + * the map results in undefined behavior. + * + * Performance begins to degrade significantly beyond a load factor of ~70%. + * For best performance, choose a capacity that will keep the load factor + * below 70%. E.g., if inserting `N` unique keys, choose a capacity of + * `N * (1/0.7)`. + * + * The `empty_key_sentinel` and `empty_value_sentinel` values are reserved and + * undefined behavior results from attempting to insert any key/value pair + * that contains either. + * + * @param capacity The total number of slots in the map + * @param empty_key_sentinel The reserved key value for empty slots + * @param empty_value_sentinel The reserved mapped value for empty slots + * @param alloc Allocator used for allocating device storage + */ + static_reduction_map(std::size_t capacity, + Key empty_key_sentinel, + Value empty_value_sentinel, + Allocator const& alloc = Allocator{}); + + /** + * @brief Destroys the map and frees its contents. + * + */ + ~static_reduction_map(); + + /** + * @brief Inserts all key/value pairs in the range `[first, last)`. + * + * If multiple keys in `[first, last)` compare equal, it is unspecified which + * element is inserted. + * + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the map's `value_type` + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of key/value pairs + * @param last End of the sequence of key/value pairs + * @param hash The unary function to apply to hash each key + * @param key_equal The binary function to compare two keys for equality + */ + template , + typename KeyEqual = thrust::equal_to> + void insert(InputIt first, InputIt last, Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}); + + /** + * @brief Finds the values corresponding to all keys in the range `[first, last)`. + * + * If the key `*(first + i)` exists in the map, copies its associated value to `(output_begin + + * i)`. Else, copies the empty value sentinel. + * + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the map's `key_type` + * @tparam OutputIt Device accessible output iterator whose `value_type` is + * convertible to the map's `mapped_type` + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param output_begin Beginning of the sequence of values retrieved for each key + * @param hash The unary function to apply to hash each key + * @param key_equal The binary function to compare two keys for equality + */ + template , + typename KeyEqual = thrust::equal_to> + void find(InputIt first, + InputIt last, + OutputIt output_begin, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}) noexcept; + + /** + * @brief Indicates whether the keys in the range `[first, last)` are contained in the map. + * + * Writes a `bool` to `(output + i)` indicating if the key `*(first + i)` exists in the map. + * + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the map's `key_type` + * @tparam OutputIt Device accessible output iterator whose `value_type` is + * convertible to the map's `mapped_type` + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param output_begin Beginning of the sequence of booleans for the presence of each key + * @param hash The unary function to apply to hash each key + * @param key_equal The binary function to compare two keys for equality + */ + template , + typename KeyEqual = thrust::equal_to> + void contains(InputIt first, + InputIt last, + OutputIt output_begin, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}) noexcept; + + private: + class device_view_base { + protected: + // Import member type definitions from `static_reduction_map` + using value_type = value_type; + using key_type = Key; + using mapped_type = Value; + using iterator = pair_atomic_type*; + using const_iterator = pair_atomic_type const*; + + private: + pair_atomic_type* slots_{}; ///< Pointer to flat slots storage + std::size_t capacity_{}; ///< Total number of slots + Key empty_key_sentinel_{}; ///< Key value that represents an empty slot + Value empty_value_sentinel_{}; ///< Initial Value of empty slot + + protected: + __host__ __device__ device_view_base(pair_atomic_type* slots, + std::size_t capacity, + Key empty_key_sentinel, + Value empty_value_sentinel) noexcept + : slots_{slots}, + capacity_{capacity}, + empty_key_sentinel_{empty_key_sentinel}, + empty_value_sentinel_{empty_value_sentinel} + { + } + + /** + * @brief Gets slots array. + * + * @return Slots array + */ + __device__ pair_atomic_type* get_slots() noexcept { return slots_; } + + /** + * @brief Gets slots array. + * + * @return Slots array + */ + __device__ pair_atomic_type const* get_slots() const noexcept { return slots_; } + + /** + * @brief Returns the initial slot for a given key `k` + * + * @tparam Hash Unary callable type + * @param k The key to get the slot for + * @param hash The unary callable used to hash the key + * @return Pointer to the initial slot for `k` + */ + template + __device__ iterator initial_slot(Key const& k, Hash hash) noexcept + { + return &slots_[hash(k) % capacity_]; + } + + /** + * @brief Returns the initial slot for a given key `k` + * + * @tparam Hash Unary callable type + * @param k The key to get the slot for + * @param hash The unary callable used to hash the key + * @return Pointer to the initial slot for `k` + */ + template + __device__ const_iterator initial_slot(Key const& k, Hash hash) const noexcept + { + return &slots_[hash(k) % capacity_]; + } + + /** + * @brief Returns the initial slot for a given key `k` + * + * To be used for Cooperative Group based probing. + * + * @tparam CG Cooperative Group type + * @tparam Hash Unary callable type + * @param g the Cooperative Group for which the initial slot is needed + * @param k The key to get the slot for + * @param hash The unary callable used to hash the key + * @return Pointer to the initial slot for `k` + */ + template + __device__ iterator initial_slot(CG g, Key const& k, Hash hash) noexcept + { + return &slots_[(hash(k) + g.thread_rank()) % capacity_]; + } + + /** + * @brief Returns the initial slot for a given key `k` + * + * To be used for Cooperative Group based probing. + * + * @tparam CG Cooperative Group type + * @tparam Hash Unary callable type + * @param g the Cooperative Group for which the initial slot is needed + * @param k The key to get the slot for + * @param hash The unary callable used to hash the key + * @return Pointer to the initial slot for `k` + */ + template + __device__ const_iterator initial_slot(CG g, Key const& k, Hash hash) const noexcept + { + return &slots_[(hash(k) + g.thread_rank()) % capacity_]; + } + + /** + * @brief Given a slot `s`, returns the next slot. + * + * If `s` is the last slot, wraps back around to the first slot. + * + * @param s The slot to advance + * @return The next slot after `s` + */ + __device__ iterator next_slot(iterator s) noexcept { return (++s < end()) ? s : begin_slot(); } + + /** + * @brief Given a slot `s`, returns the next slot. + * + * If `s` is the last slot, wraps back around to the first slot. + * + * @param s The slot to advance + * @return The next slot after `s` + */ + __device__ const_iterator next_slot(const_iterator s) const noexcept + { + return (++s < end()) ? s : begin_slot(); + } + + /** + * @brief Given a slot `s`, returns the next slot. + * + * If `s` is the last slot, wraps back around to the first slot. To + * be used for Cooperative Group based probing. + * + * @tparam CG The Cooperative Group type + * @param g The Cooperative Group for which the next slot is needed + * @param s The slot to advance + * @return The next slot after `s` + */ + template + __device__ iterator next_slot(CG g, iterator s) noexcept + { + uint32_t index = s - slots_; + return &slots_[(index + g.size()) % capacity_]; + } + + /** + * @brief Given a slot `s`, returns the next slot. + * + * If `s` is the last slot, wraps back around to the first slot. To + * be used for Cooperative Group based probing. + * + * @tparam CG The Cooperative Group type + * @param g The Cooperative Group for which the next slot is needed + * @param s The slot to advance + * @return The next slot after `s` + */ + template + __device__ const_iterator next_slot(CG g, const_iterator s) const noexcept + { + uint32_t index = s - slots_; + return &slots_[(index + g.size()) % capacity_]; + } + + public: + /** + * @brief Gets the maximum number of elements the hash map can hold. + * + * @return The maximum number of elements the hash map can hold + */ + __host__ __device__ std::size_t get_capacity() const noexcept { return capacity_; } + + /** + * @brief Gets the sentinel value used to represent an empty key slot. + * + * @return The sentinel value used to represent an empty key slot + */ + __host__ __device__ Key get_empty_key_sentinel() const noexcept { return empty_key_sentinel_; } + + /** + * @brief Gets the sentinel value used to represent an empty value slot. + * + * @return The sentinel value used to represent an empty value slot + */ + __host__ __device__ Value get_empty_value_sentinel() const noexcept + { + return empty_value_sentinel_; + } + + /** + * @brief Returns iterator to the first slot. + * + * @note Unlike `std::map::begin()`, the `begin_slot()` iterator does _not_ point to the first + * occupied slot. Instead, it refers to the first slot in the array of contiguous slot storage. + * Iterating from `begin_slot()` to `end_slot()` will iterate over all slots, including those + * both empty and filled. + * + * There is no `begin()` iterator to avoid confusion as it is not possible to provide an + * iterator over only the filled slots. + * + * @return Iterator to the first slot + */ + __device__ iterator begin_slot() noexcept { return slots_; } + + /** + * @brief Returns iterator to the first slot. + * + * @note Unlike `std::map::begin()`, the `begin_slot()` iterator does _not_ point to the first + * occupied slot. Instead, it refers to the first slot in the array of contiguous slot storage. + * Iterating from `begin_slot()` to `end_slot()` will iterate over all slots, including those + * both empty and filled. + * + * There is no `begin()` iterator to avoid confusion as it is not possible to provide an + * iterator over only the filled slots. + * + * @return Iterator to the first slot + */ + __device__ const_iterator begin_slot() const noexcept { return slots_; } + + /** + * @brief Returns a const_iterator to one past the last slot. + * + * @return A const_iterator to one past the last slot + */ + __host__ __device__ const_iterator end_slot() const noexcept { return slots_ + capacity_; } + + /** + * @brief Returns an iterator to one past the last slot. + * + * @return An iterator to one past the last slot + */ + __host__ __device__ iterator end_slot() noexcept { return slots_ + capacity_; } + + /** + * @brief Returns a const_iterator to one past the last slot. + * + * `end()` calls `end_slot()` and is provided for convenience for those familiar with checking + * an iterator returned from `find()` against the `end()` iterator. + * + * @return A const_iterator to one past the last slot + */ + __host__ __device__ const_iterator end() const noexcept { return end_slot(); } + + /** + * @brief Returns an iterator to one past the last slot. + * + * `end()` calls `end_slot()` and is provided for convenience for those familiar with checking + * an iterator returned from `find()` against the `end()` iterator. + * + * @return An iterator to one past the last slot + */ + __host__ __device__ iterator end() noexcept { return end_slot(); } + }; + + public: + /** + * @brief Mutable, non-owning view-type that may be used in device code to + * perform singular inserts into the map. + * + * `device_mutable_view` is trivially-copyable and is intended to be passed by + * value. + * + * Example: + * \code{.cpp} + * cuco::static_reduction_map m{100'000, -1, -1}; + * + * // Inserts a sequence of pairs {{0,0}, {1,1}, ... {i,i}} + * thrust::for_each(thrust::make_counting_iterator(0), + * thrust::make_counting_iterator(50'000), + * [map = m.get_mutable_device_view()] + * __device__ (auto i) mutable { + * map.insert(thrust::make_pair(i,i)); + * }); + * \endcode + */ + class device_mutable_view : public device_view_base { + public: + using value_type = typename device_view_base::value_type; + using key_type = typename device_view_base::key_type; + using mapped_type = typename device_view_base::mapped_type; + using iterator = typename device_view_base::iterator; + using const_iterator = typename device_view_base::const_iterator; + /** + * @brief Construct a mutable view of the first `capacity` slots of the + * slots array pointed to by `slots`. + * + * @param slots Pointer to beginning of initialized slots array + * @param capacity The number of slots viewed by this object + * @param empty_key_sentinel The reserved value for keys to represent empty + * slots + * @param empty_value_sentinel The reserved value for mapped values to + * represent empty slots + */ + __host__ __device__ device_mutable_view(pair_atomic_type* slots, + std::size_t capacity, + Key empty_key_sentinel, + Value empty_value_sentinel) noexcept + : device_view_base{slots, capacity, empty_key_sentinel, empty_value_sentinel} + { + } + + /** + * @brief Inserts the specified key/value pair into the map. + * + * Returns a pair consisting of an iterator to the inserted element (or to + * the element that prevented the insertion) and a `bool` denoting whether + * the insertion took place. + * + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param insert_pair The pair to insert + * @param hash The unary callable used to hash the key + * @param key_equal The binary callable used to compare two keys for + * equality + * @return `true` if the insert was successful, `false` otherwise. + */ + template , + typename KeyEqual = thrust::equal_to> + __device__ bool insert(value_type const& insert_pair, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}) noexcept; + /** + * @brief Inserts the specified key/value pair into the map. + * + * Returns a pair consisting of an iterator to the inserted element (or to + * the element that prevented the insertion) and a `bool` denoting whether + * the insertion took place. Uses the CUDA Cooperative Groups API to + * to leverage multiple threads to perform a single insert. This provides a + * significant boost in throughput compared to the non Cooperative Group + * `insert` at moderate to high load factors. + * + * @tparam Cooperative Group type + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * + * @param g The Cooperative Group that performs the insert + * @param insert_pair The pair to insert + * @param hash The unary callable used to hash the key + * @param key_equal The binary callable used to compare two keys for + * equality + * @return `true` if the insert was successful, `false` otherwise. + */ + template , + typename KeyEqual = thrust::equal_to> + __device__ bool insert(CG g, + value_type const& insert_pair, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}) noexcept; + + }; // class device mutable view + + /** + * @brief Non-owning view-type that may be used in device code to + * perform singular find and contains operations for the map. + * + * `device_view` is trivially-copyable and is intended to be passed by + * value. + * + */ + class device_view : public device_view_base { + public: + using value_type = typename device_view_base::value_type; + using key_type = typename device_view_base::key_type; + using mapped_type = typename device_view_base::mapped_type; + using iterator = typename device_view_base::iterator; + using const_iterator = typename device_view_base::const_iterator; + /** + * @brief Construct a view of the first `capacity` slots of the + * slots array pointed to by `slots`. + * + * @param slots Pointer to beginning of initialized slots array + * @param capacity The number of slots viewed by this object + * @param empty_key_sentinel The reserved value for keys to represent empty + * slots + * @param empty_value_sentinel The reserved value for mapped values to + * represent empty slots + */ + __host__ __device__ device_view(pair_atomic_type* slots, + std::size_t capacity, + Key empty_key_sentinel, + Value empty_value_sentinel) noexcept + : device_view_base{slots, capacity, empty_key_sentinel, empty_value_sentinel} + { + } + + /** + * @brief Makes a copy of given `device_view` using non-owned memory. + * + * This function is intended to be used to create shared memory copies of small static maps, + * although global memory can be used as well. + * + * Example: + * @code{.cpp} + * template + * __global__ void use_device_view(const typename MapType::device_view device_view, + * map_key_t const* const keys_to_search, + * map_value_t* const values_found, + * const size_t number_of_elements) + * { + * const size_t index = blockIdx.x * blockDim.x + threadIdx.x; + * + * __shared__ typename MapType::pair_atomic_type sm_buffer[CAPACITY]; + * + * auto g = cg::this_thread_block(); + * + * const map_t::device_view sm_static_reduction_map = device_view.make_copy(g, + * sm_buffer); + * + * for (size_t i = g.thread_rank(); i < number_of_elements; i += g.size()) + * { + * values_found[i] = sm_static_reduction_map.find(keys_to_search[i])->second; + * } + * } + * @endcode + * + * @tparam CG The type of the cooperative thread group + * @param g The ooperative thread group used to copy the slots + * @param source_device_view `device_view` to copy from + * @param memory_to_use Array large enough to support `capacity` elements. Object does not take + * the ownership of the memory + * @return Copy of passed `device_view` + */ + template + __device__ static device_view make_copy(CG g, + pair_atomic_type* const memory_to_use, + device_view source_device_view) noexcept + { +#ifndef CUDART_VERSION +#error CUDART_VERSION Undefined! +#elif (CUDART_VERSION >= 11000) + __shared__ cuda::barrier barrier; + if (g.thread_rank() == 0) { init(&barrier, g.size()); } + g.sync(); + + cuda::memcpy_async(g, + memory_to_use, + source_device_view.get_slots(), + sizeof(pair_atomic_type) * source_device_view.get_capacity(), + barrier); + + barrier.arrive_and_wait(); +#else + pair_atomic_type const* const slots_ptr = source_device_view.get_slots(); + for (std::size_t i = g.thread_rank(); i < source_device_view.get_capacity(); i += g.size()) { + new (&memory_to_use[i].first) + atomic_key_type{slots_ptr[i].first.load(cuda::memory_order_relaxed)}; + new (&memory_to_use[i].second) + atomic_mapped_type{slots_ptr[i].second.load(cuda::memory_order_relaxed)}; + } + g.sync(); +#endif + + return device_view(memory_to_use, + source_device_view.get_capacity(), + source_device_view.get_empty_key_sentinel(), + source_device_view.get_empty_value_sentinel()); + } + + /** + * @brief Finds the value corresponding to the key `k`. + * + * Returns an iterator to the pair whose key is equivalent to `k`. + * If no such pair exists, returns `end()`. + * + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param k The key to search for + * @param hash The unary callable used to hash the key + * @param key_equal The binary callable used to compare two keys + * for equality + * @return An iterator to the position at which the key/value pair + * containing `k` was inserted + */ + template , + typename KeyEqual = thrust::equal_to> + __device__ iterator find(Key const& k, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}) noexcept; + + /** @brief Finds the value corresponding to the key `k`. + * + * Returns a const_iterator to the pair whose key is equivalent to `k`. + * If no such pair exists, returns `end()`. + * + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param k The key to search for + * @param hash The unary callable used to hash the key + * @param key_equal The binary callable used to compare two keys + * for equality + * @return An iterator to the position at which the key/value pair + * containing `k` was inserted + */ + template , + typename KeyEqual = thrust::equal_to> + __device__ const_iterator find(Key const& k, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}) const noexcept; + + /** + * @brief Finds the value corresponding to the key `k`. + * + * Returns an iterator to the pair whose key is equivalent to `k`. + * If no such pair exists, returns `end()`. Uses the CUDA Cooperative Groups API to + * to leverage multiple threads to perform a single find. This provides a + * significant boost in throughput compared to the non Cooperative Group + * `find` at moderate to high load factors. + * + * @tparam CG Cooperative Group type + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param g The Cooperative Group used to perform the find + * @param k The key to search for + * @param hash The unary callable used to hash the key + * @param key_equal The binary callable used to compare two keys + * for equality + * @return An iterator to the position at which the key/value pair + * containing `k` was inserted + */ + template , + typename KeyEqual = thrust::equal_to> + __device__ iterator + find(CG g, Key const& k, Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}) noexcept; + + /** + * @brief Finds the value corresponding to the key `k`. + * + * Returns a const_iterator to the pair whose key is equivalent to `k`. + * If no such pair exists, returns `end()`. Uses the CUDA Cooperative Groups API to + * to leverage multiple threads to perform a single find. This provides a + * significant boost in throughput compared to the non Cooperative Group + * `find` at moderate to high load factors. + * + * @tparam CG Cooperative Group type + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param g The Cooperative Group used to perform the find + * @param k The key to search for + * @param hash The unary callable used to hash the key + * @param key_equal The binary callable used to compare two keys + * for equality + * @return An iterator to the position at which the key/value pair + * containing `k` was inserted + */ + template , + typename KeyEqual = thrust::equal_to> + __device__ const_iterator + find(CG g, Key const& k, Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}) const noexcept; + + /** + * @brief Indicates whether the key `k` was inserted into the map. + * + * If the key `k` was inserted into the map, find returns + * true. Otherwise, it returns false. + * + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param k The key to search for + * @param hash The unary callable used to hash the key + * @param key_equal The binary callable used to compare two keys + * for equality + * @return A boolean indicating whether the key/value pair + * containing `k` was inserted + */ + template , + typename KeyEqual = thrust::equal_to> + __device__ bool contains(Key const& k, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}) noexcept; + + /** + * @brief Indicates whether the key `k` was inserted into the map. + * + * If the key `k` was inserted into the map, find returns + * true. Otherwise, it returns false. Uses the CUDA Cooperative Groups API to + * to leverage multiple threads to perform a single contains operation. This provides a + * significant boost in throughput compared to the non Cooperative Group + * `contains` at moderate to high load factors. + * + * @tparam CG Cooperative Group type + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param g The Cooperative Group used to perform the contains operation + * @param k The key to search for + * @param hash The unary callable used to hash the key + * @param key_equal The binary callable used to compare two keys + * for equality + * @return A boolean indicating whether the key/value pair + * containing `k` was inserted + */ + template , + typename KeyEqual = thrust::equal_to> + __device__ bool contains(CG g, + Key const& k, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}) noexcept; + }; // class device_view + + /** + * @brief Gets the maximum number of elements the hash map can hold. + * + * @return The maximum number of elements the hash map can hold + */ + std::size_t get_capacity() const noexcept { return capacity_; } + + /** + * @brief Gets the number of elements in the hash map. + * + * @return The number of elements in the map + */ + std::size_t get_size() const noexcept { return size_; } + + /** + * @brief Gets the load factor of the hash map. + * + * @return The load factor of the hash map + */ + float get_load_factor() const noexcept { return static_cast(size_) / capacity_; } + + /** + * @brief Gets the sentinel value used to represent an empty key slot. + * + * @return The sentinel value used to represent an empty key slot + */ + Key get_empty_key_sentinel() const noexcept { return empty_key_sentinel_; } + + /** + * @brief Gets the sentinel value used to represent an empty value slot. + * + * @return The sentinel value used to represent an empty value slot + */ + Value get_empty_value_sentinel() const noexcept { return empty_value_sentinel_; } + + /** + * @brief Constructs a device_view object based on the members of the `static_reduction_map` + * object. + * + * @return A device_view object based on the members of the `static_reduction_map` object + */ + device_view get_device_view() const noexcept + { + return device_view(slots_, capacity_, empty_key_sentinel_, empty_value_sentinel_); + } + + /** + * @brief Constructs a device_mutable_view object based on the members of the + * `static_reduction_map` object + * + * @return A device_mutable_view object based on the members of the `static_reduction_map` object + */ + device_mutable_view get_device_mutable_view() const noexcept + { + return device_mutable_view(slots_, capacity_, empty_key_sentinel_, empty_value_sentinel_); + } + + private: + pair_atomic_type* slots_{nullptr}; ///< Pointer to flat slots storage + std::size_t capacity_{}; ///< Total number of slots + std::size_t size_{}; ///< Number of keys in map + Key empty_key_sentinel_{}; ///< Key value that represents an empty slot + Value empty_value_sentinel_{}; ///< Initial value of empty slot + atomic_ctr_type* num_successes_{}; ///< Number of successfully inserted keys on insert + slot_allocator_type slot_allocator_{}; ///< Allocator used to allocate slots +}; +} // namespace cuco + +#include \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 66c1682ed..32d77b2a8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,13 +48,9 @@ endfunction(ConfigureTest) ################################################################################################### ### test sources ################################################################################## ################################################################################################### -set(STATIC_MAP_TEST_SRC - "${CMAKE_CURRENT_SOURCE_DIR}/static_map/static_map_test.cu") -ConfigureTest(STATIC_MAP_TEST "${STATIC_MAP_TEST_SRC}") -#################################################################################################### -set(DYNAMIC_MAP_TEST_SRC - "${CMAKE_CURRENT_SOURCE_DIR}/dynamic_map/dynamic_map_test.cu") +ConfigureTest(STATIC_MAP_TEST "${CMAKE_CURRENT_SOURCE_DIR}/static_map/static_map_test.cu") -ConfigureTest(DYNAMIC_MAP_TEST "${DYNAMIC_MAP_TEST_SRC}") -#################################################################################################### \ No newline at end of file +ConfigureTest(STATIC_REDUCTION_MAP_TEST "${CMAKE_CURRENT_SOURCE_DIR}/static_reduction_map/static_reduction_map_test.cu") + +ConfigureTest(DYNAMIC_MAP_TEST "${CMAKE_CURRENT_SOURCE_DIR}/dynamic_map/dynamic_map_test.cu") \ No newline at end of file diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu new file mode 100644 index 000000000..d69d581fc --- /dev/null +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2020, 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 + +namespace { +namespace cg = cooperative_groups; + +// Thrust logical algorithms (any_of/all_of/none_of) don't work with device +// lambdas: See https://github.com/thrust/thrust/issues/1062 +template +bool all_of(Iterator begin, Iterator end, Predicate p) +{ + auto size = thrust::distance(begin, end); + return size == thrust::count_if(begin, end, p); +} + +template +bool any_of(Iterator begin, Iterator end, Predicate p) +{ + return thrust::count_if(begin, end, p) > 0; +} + +template +bool none_of(Iterator begin, Iterator end, Predicate p) +{ + return not all_of(begin, end, p); +} +} // namespace + +enum class dist_type { UNIQUE, UNIFORM, GAUSSIAN }; + +template +static void generate_keys(OutputIt output_begin, OutputIt output_end) +{ + auto num_keys = std::distance(output_begin, output_end); + + std::random_device rd; + std::mt19937 gen{rd()}; + + switch (Dist) { + case dist_type::UNIQUE: + for (auto i = 0; i < num_keys; ++i) { + output_begin[i] = i; + } + break; + case dist_type::UNIFORM: + for (auto i = 0; i < num_keys; ++i) { + output_begin[i] = std::abs(static_cast(gen())); + } + break; + case dist_type::GAUSSIAN: + std::normal_distribution<> dg{1e9, 1e7}; + for (auto i = 0; i < num_keys; ++i) { + output_begin[i] = std::abs(static_cast(dg(gen))); + } + break; + } +} + +TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", + "", + ((typename T, dist_type Dist), T, Dist), + (int32_t, dist_type::UNIQUE), + (int64_t, dist_type::UNIQUE), + (int32_t, dist_type::UNIFORM), + (int64_t, dist_type::UNIFORM), + (int32_t, dist_type::GAUSSIAN), + (int64_t, dist_type::GAUSSIAN)) +{ + using Key = T; + using Value = T; + + constexpr std::size_t num_keys{50'000'000}; + cuco::static_reduction_map map{100'000'000, -1, -1}; + + auto m_view = map.get_device_mutable_view(); + auto view = map.get_device_view(); + + std::vector h_keys(num_keys); + std::vector h_values(num_keys); + std::vector> h_pairs(num_keys); + + generate_keys(h_keys.begin(), h_keys.end()); + + for (auto i = 0; i < num_keys; ++i) { + Key key = h_keys[i]; + Value val = h_keys[i]; + h_pairs[i].first = key; + h_pairs[i].second = val; + h_values[i] = val; + } + + thrust::device_vector d_keys(h_keys); + thrust::device_vector d_values(h_values); + thrust::device_vector> d_pairs(h_pairs); + thrust::device_vector d_results(num_keys); + thrust::device_vector d_contained(num_keys); + + // bulk function test cases + SECTION("All inserted keys-value pairs should be correctly recovered during find") + { + map.insert(d_pairs.begin(), d_pairs.end()); + map.find(d_keys.begin(), d_keys.end(), d_results.begin()); + auto zip = thrust::make_zip_iterator(thrust::make_tuple(d_results.begin(), d_values.begin())); + + REQUIRE(all_of(zip, zip + num_keys, [] __device__(auto const& p) { + return thrust::get<0>(p) == thrust::get<1>(p); + })); + } + + SECTION("All inserted keys-value pairs should be contained") + { + map.insert(d_pairs.begin(), d_pairs.end()); + map.contains(d_keys.begin(), d_keys.end(), d_contained.begin()); + + REQUIRE( + all_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); + } + + SECTION("Non-inserted keys-value pairs should not be contained") + { + map.contains(d_keys.begin(), d_keys.end(), d_contained.begin()); + + REQUIRE( + none_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); + } + + SECTION("Inserting unique keys should return insert success.") + { + if (Dist == dist_type::UNIQUE) { + REQUIRE(all_of(d_pairs.begin(), + d_pairs.end(), + [m_view] __device__(cuco::pair_type const& pair) mutable { + return m_view.insert(pair); + })); + } + } + + SECTION("Cannot find any key in an empty hash map with non-const view") + { + SECTION("non-const view") + { + REQUIRE(all_of(d_pairs.begin(), + d_pairs.end(), + [view] __device__(cuco::pair_type const& pair) mutable { + return view.find(pair.first) == view.end(); + })); + } + SECTION("const view") + { + REQUIRE(all_of( + d_pairs.begin(), d_pairs.end(), [view] __device__(cuco::pair_type const& pair) { + return view.find(pair.first) == view.end(); + })); + } + } + + SECTION("Keys are all found after inserting many keys.") + { + // Bulk insert keys + thrust::for_each(thrust::device, + d_pairs.begin(), + d_pairs.end(), + [m_view] __device__(cuco::pair_type const& pair) mutable { + m_view.insert(pair); + }); + + SECTION("non-const view") + { + // All keys should be found + REQUIRE(all_of(d_pairs.begin(), + d_pairs.end(), + [view] __device__(cuco::pair_type const& pair) mutable { + auto const found = view.find(pair.first); + return (found != view.end()) and (found->first.load() == pair.first and + found->second.load() == pair.second); + })); + } + SECTION("const view") + { + // All keys should be found + REQUIRE(all_of( + d_pairs.begin(), d_pairs.end(), [view] __device__(cuco::pair_type const& pair) { + auto const found = view.find(pair.first); + return (found != view.end()) and + (found->first.load() == pair.first and found->second.load() == pair.second); + })); + } + } +} + +template +__global__ void shared_memory_test_kernel( + typename MapType::device_view const* const device_views, + typename MapType::device_view::key_type const* const insterted_keys, + typename MapType::device_view::mapped_type const* const inserted_values, + const size_t number_of_elements, + bool* const keys_exist, + bool* const keys_and_values_correct) +{ + // Each block processes one map + const size_t map_id = blockIdx.x; + const size_t offset = map_id * number_of_elements; + + __shared__ typename MapType::pair_atomic_type sm_buffer[CAPACITY]; + + auto g = cg::this_thread_block(); + typename MapType::device_view sm_device_view = + MapType::device_view::make_copy(g, sm_buffer, device_views[map_id]); + + for (int i = g.thread_rank(); i < number_of_elements; i += g.size()) { + auto found_pair_it = sm_device_view.find(insterted_keys[offset + i]); + + if (found_pair_it != sm_device_view.end()) { + keys_exist[offset + i] = true; + if (found_pair_it->first == insterted_keys[offset + i] and + found_pair_it->second == inserted_values[offset + i]) { + keys_and_values_correct[offset + i] = true; + } else { + keys_and_values_correct[offset + i] = false; + } + } else { + keys_exist[offset + i] = false; + keys_and_values_correct[offset + i] = true; + } + } +} + +TEMPLATE_TEST_CASE_SIG("Shared memory static map", + "", + ((typename T, dist_type Dist), T, Dist), + (int32_t, dist_type::UNIQUE), + (int64_t, dist_type::UNIQUE), + (int32_t, dist_type::UNIFORM), + (int64_t, dist_type::UNIFORM), + (int32_t, dist_type::GAUSSIAN), + (int64_t, dist_type::GAUSSIAN)) +{ + using KeyType = T; + using ValueType = T; + using MapType = cuco::static_reduction_map; + using DeviceViewType = typename MapType::device_view; + using DeviceViewIteratorType = typename DeviceViewType::iterator; + + constexpr std::size_t number_of_maps = 1000; + constexpr std::size_t elements_in_map = 500; + constexpr std::size_t map_capacity = 2 * elements_in_map; + + // one array for all maps, first elements_in_map element belong to map 0, second to map 1 and so + // on + std::vector h_keys(number_of_maps * elements_in_map); + std::vector h_values(number_of_maps * elements_in_map); + std::vector> h_pairs(number_of_maps * elements_in_map); + + // using std::unique_ptr because static_reduction_map does not have copy/move + // constructor/assignment operator yet + std::vector> maps; + + for (std::size_t map_id = 0; map_id < number_of_maps; ++map_id) { + const std::size_t offset = map_id * elements_in_map; + + generate_keys(h_keys.begin() + offset, + h_keys.begin() + offset + elements_in_map); + + for (std::size_t i = 0; i < elements_in_map; ++i) { + KeyType key = h_keys[offset + i]; + ValueType val = key < std::numeric_limits::max() ? key + 1 : 0; + h_values[offset + i] = val; + h_pairs[offset + i].first = key; + h_pairs[offset + i].second = val; + } + + maps.push_back(std::make_unique(map_capacity, -1, -1)); + } + + thrust::device_vector d_keys(h_keys); + thrust::device_vector d_values(h_values); + thrust::device_vector> d_pairs(h_pairs); + + SECTION("Keys are all found after insertion.") + { + std::vector h_device_views; + for (std::size_t map_id = 0; map_id < number_of_maps; ++map_id) { + const std::size_t offset = map_id * elements_in_map; + + MapType* map = maps[map_id].get(); + map->insert(d_pairs.begin() + offset, d_pairs.begin() + offset + elements_in_map); + h_device_views.push_back(map->get_device_view()); + } + thrust::device_vector d_device_views(h_device_views); + + thrust::device_vector d_keys_exist(number_of_maps * elements_in_map); + thrust::device_vector d_keys_and_values_correct(number_of_maps * elements_in_map); + + shared_memory_test_kernel + <<>>(d_device_views.data().get(), + d_keys.data().get(), + d_values.data().get(), + elements_in_map, + d_keys_exist.data().get(), + d_keys_and_values_correct.data().get()); + + REQUIRE(d_keys_exist.size() == d_keys_and_values_correct.size()); + auto zip = thrust::make_zip_iterator( + thrust::make_tuple(d_keys_exist.begin(), d_keys_and_values_correct.begin())); + + REQUIRE(all_of(zip, zip + d_keys_exist.size(), [] __device__(auto const& z) { + return thrust::get<0>(z) and thrust::get<1>(z); + })); + } + + SECTION("No key is found before insertion.") + { + std::vector h_device_views; + for (std::size_t map_id = 0; map_id < number_of_maps; ++map_id) { + h_device_views.push_back(maps[map_id].get()->get_device_view()); + } + thrust::device_vector d_device_views(h_device_views); + + thrust::device_vector d_keys_exist(number_of_maps * elements_in_map); + thrust::device_vector d_keys_and_values_correct(number_of_maps * elements_in_map); + + shared_memory_test_kernel + <<>>(d_device_views.data().get(), + d_keys.data().get(), + d_values.data().get(), + elements_in_map, + d_keys_exist.data().get(), + d_keys_and_values_correct.data().get()); + + REQUIRE(none_of(d_keys_exist.begin(), d_keys_exist.end(), [] __device__(const bool key_found) { + return key_found; + })); + } +} \ No newline at end of file From fe606cd60d27b645d2c551fb607652658c204c41 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Mon, 4 Jan 2021 14:57:20 -0600 Subject: [PATCH 02/69] Add template parameter for reduction binary op. --- include/cuco/detail/static_reduction_map.inl | 172 +++++++----- include/cuco/static_reduction_map.cuh | 54 ++-- .../static_reduction_map_test.cu | 263 +----------------- 3 files changed, 134 insertions(+), 355 deletions(-) diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index 243032f6b..be28e0f28 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -26,14 +26,17 @@ enum class insert_result { DUPLICATE ///< Insert did not succeed, key is already present }; -template -static_reduction_map::static_reduction_map(std::size_t capacity, - Key empty_key_sentinel, - Value empty_value_sentinel, - Allocator const& alloc) +template +static_reduction_map::static_reduction_map( + std::size_t capacity, Key empty_key_sentinel, ReductionOp reduction_op, Allocator const& alloc) : capacity_{capacity}, empty_key_sentinel_{empty_key_sentinel}, - empty_value_sentinel_{empty_value_sentinel}, + empty_value_sentinel_{ReductionOp::identity}, + op_{reduction_op}, slot_allocator_{alloc} { slots_ = std::allocator_traits::allocate(slot_allocator_, capacity); @@ -41,25 +44,33 @@ static_reduction_map::static_reduction_map(std::si auto constexpr block_size = 256; auto constexpr stride = 4; auto const grid_size = (capacity + stride * block_size - 1) / (stride * block_size); - detail::initialize - <<>>(slots_, empty_key_sentinel, empty_value_sentinel, capacity); + detail::initialize<<>>( + slots_, get_empty_key_sentinel(), get_empty_value_sentinel(), get_capacity()); CUCO_CUDA_TRY(cudaMallocManaged(&num_successes_, sizeof(atomic_ctr_type))); } -template -static_reduction_map::~static_reduction_map() +template +static_reduction_map::~static_reduction_map() { std::allocator_traits::deallocate(slot_allocator_, slots_, capacity_); CUCO_CUDA_TRY(cudaFree(num_successes_)); } -template +template template -void static_reduction_map::insert(InputIt first, - InputIt last, - Hash hash, - KeyEqual key_equal) +void static_reduction_map::insert(InputIt first, + InputIt last, + Hash hash, + KeyEqual key_equal) { auto num_keys = std::distance(first, last); auto const block_size = 128; @@ -80,9 +91,13 @@ void static_reduction_map::insert(InputIt first, size_ += num_successes_->load(cuda::std::memory_order_relaxed); } -template +template template -void static_reduction_map::find( +void static_reduction_map::find( InputIt first, InputIt last, OutputIt output_begin, Hash hash, KeyEqual key_equal) noexcept { auto num_keys = std::distance(first, last); @@ -97,9 +112,13 @@ void static_reduction_map::find( CUCO_CUDA_TRY(cudaDeviceSynchronize()); } -template +template template -void static_reduction_map::contains( +void static_reduction_map::contains( InputIt first, InputIt last, OutputIt output_begin, Hash hash, KeyEqual key_equal) noexcept { auto num_keys = std::distance(first, last); @@ -114,9 +133,14 @@ void static_reduction_map::contains( CUCO_CUDA_TRY(cudaDeviceSynchronize()); } -template +template template -__device__ bool static_reduction_map::device_mutable_view::insert( +__device__ Value +static_reduction_map::device_mutable_view::insert( value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept { auto current_slot{initial_slot(insert_pair.first, hash)}; @@ -128,26 +152,12 @@ __device__ bool static_reduction_map::device_mutab auto& slot_key = current_slot->first; auto& slot_value = current_slot->second; - bool key_success = + auto const key_success = slot_key.compare_exchange_strong(expected_key, insert_pair.first, memory_order_relaxed); - bool value_success = - slot_value.compare_exchange_strong(expected_value, insert_pair.second, memory_order_relaxed); - - if (key_success) { - while (not value_success) { - value_success = - slot_value.compare_exchange_strong(expected_value = this->get_empty_value_sentinel(), - insert_pair.second, - memory_order_relaxed); - } - return true; - } else if (value_success) { - slot_value.store(this->get_empty_value_sentinel(), memory_order_relaxed); - } - // if the key was already inserted by another thread, than this instance is a - // duplicate, so the insert fails - if (key_equal(insert_pair.first, expected_key)) { return false; } + if (key_success or key_equal(insert_pair.first, expected_key)) { + // return do_op{}(slot_value, insert_pair.second); + } // if we couldn't insert the key, but it wasn't a duplicate, then there must // have been some other key there, so we keep looking for a slot @@ -155,9 +165,14 @@ __device__ bool static_reduction_map::device_mutab } } -template +template template -__device__ bool static_reduction_map::device_mutable_view::insert( +__device__ bool +static_reduction_map::device_mutable_view::insert( CG g, value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(g, insert_pair.first, hash); @@ -232,12 +247,16 @@ __device__ bool static_reduction_map::device_mutab } } -template +template template -__device__ typename static_reduction_map::device_view::iterator -static_reduction_map::device_view::find(Key const& k, - Hash hash, - KeyEqual key_equal) noexcept +__device__ + typename static_reduction_map::device_view::iterator + static_reduction_map::device_view::find( + Key const& k, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(k, hash); @@ -253,13 +272,16 @@ static_reduction_map::device_view::find(Key const& } } -template +template template -__device__ typename static_reduction_map::device_view::const_iterator -static_reduction_map::device_view::find(Key const& k, - Hash hash, - KeyEqual key_equal) const - noexcept +__device__ typename static_reduction_map::device_view:: + const_iterator + static_reduction_map::device_view::find( + Key const& k, Hash hash, KeyEqual key_equal) const noexcept { auto current_slot = initial_slot(k, hash); @@ -275,13 +297,16 @@ static_reduction_map::device_view::find(Key const& } } -template +template template -__device__ typename static_reduction_map::device_view::iterator -static_reduction_map::device_view::find(CG g, - Key const& k, - Hash hash, - KeyEqual key_equal) noexcept +__device__ + typename static_reduction_map::device_view::iterator + static_reduction_map::device_view::find( + CG g, Key const& k, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(g, k, hash); @@ -312,11 +337,16 @@ static_reduction_map::device_view::find(CG g, } } -template +template template -__device__ typename static_reduction_map::device_view::const_iterator -static_reduction_map::device_view::find( - CG g, Key const& k, Hash hash, KeyEqual key_equal) const noexcept +__device__ typename static_reduction_map::device_view:: + const_iterator + static_reduction_map::device_view::find( + CG g, Key const& k, Hash hash, KeyEqual key_equal) const noexcept { auto current_slot = initial_slot(g, k, hash); @@ -349,9 +379,14 @@ static_reduction_map::device_view::find( } } -template +template template -__device__ bool static_reduction_map::device_view::contains( +__device__ bool +static_reduction_map::device_view::contains( Key const& k, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(k, hash); @@ -367,9 +402,14 @@ __device__ bool static_reduction_map::device_view: } } -template +template template -__device__ bool static_reduction_map::device_view::contains( +__device__ bool +static_reduction_map::device_view::contains( CG g, Key const& k, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(g, k, hash); diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 241ef480d..d66c6cf4a 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -38,22 +38,16 @@ namespace cuco { -/** - * @brief Possible reduction operations that can be performed by a `static_reduction_map`. - * - * `GENERIC` allows for any associative binary reduction operation, but may have worse performance - * compared to one of the native operations. - * - */ -enum class reduction_op { - SUM, ///< Addition - SUB, ///< Subtraction - MIN, ///< Minimum value - MAX, ///< Maximum value - AND, ///< Bitwise AND - OR, ///< Bitwise OR - XOR, ///< Bitwise XOR - GENERIC ///< User-defined, associative binary operation +template +struct reduce_add { + using value_type = T; + static constexpr T identity = 0; + + template + T apply(cuda::atomic& slot, T2 const& value) + { + return slot.fetch_add(value); + } }; /** @@ -122,7 +116,8 @@ enum class reduction_op { * individual threads. * @tparam Allocator Type of allocator used for device storage */ -template > @@ -171,8 +166,8 @@ class static_reduction_map { */ static_reduction_map(std::size_t capacity, Key empty_key_sentinel, - Value empty_value_sentinel, - Allocator const& alloc = Allocator{}); + ReductionOp reduction_op = {}, + Allocator const& alloc = Allocator{}); /** * @brief Destroys the map and frees its contents. @@ -270,16 +265,18 @@ class static_reduction_map { std::size_t capacity_{}; ///< Total number of slots Key empty_key_sentinel_{}; ///< Key value that represents an empty slot Value empty_value_sentinel_{}; ///< Initial Value of empty slot + ReductionOp op_{}; ///< Binary operation reduction function object protected: __host__ __device__ device_view_base(pair_atomic_type* slots, std::size_t capacity, Key empty_key_sentinel, - Value empty_value_sentinel) noexcept + ReductionOp reduction_op) noexcept : slots_{slots}, capacity_{capacity}, empty_key_sentinel_{empty_key_sentinel}, - empty_value_sentinel_{empty_value_sentinel} + empty_value_sentinel_{ReductionOp::identity}, + op_{reduction_op} { } @@ -552,8 +549,8 @@ class static_reduction_map { __host__ __device__ device_mutable_view(pair_atomic_type* slots, std::size_t capacity, Key empty_key_sentinel, - Value empty_value_sentinel) noexcept - : device_view_base{slots, capacity, empty_key_sentinel, empty_value_sentinel} + ReductionOp reduction_op = {}) noexcept + : device_view_base{slots, capacity, empty_key_sentinel, reduction_op} { } @@ -574,9 +571,9 @@ class static_reduction_map { */ template , typename KeyEqual = thrust::equal_to> - __device__ bool insert(value_type const& insert_pair, - Hash hash = Hash{}, - KeyEqual key_equal = KeyEqual{}) noexcept; + __device__ Value insert(value_type const& insert_pair, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}) noexcept; /** * @brief Inserts the specified key/value pair into the map. * @@ -637,8 +634,8 @@ class static_reduction_map { __host__ __device__ device_view(pair_atomic_type* slots, std::size_t capacity, Key empty_key_sentinel, - Value empty_value_sentinel) noexcept - : device_view_base{slots, capacity, empty_key_sentinel, empty_value_sentinel} + ReductionOp reduction_op = {}) noexcept + : device_view_base{slots, capacity, empty_key_sentinel, reduction_op} { } @@ -922,6 +919,7 @@ class static_reduction_map { Key empty_key_sentinel_{}; ///< Key value that represents an empty slot Value empty_value_sentinel_{}; ///< Initial value of empty slot atomic_ctr_type* num_successes_{}; ///< Number of successfully inserted keys on insert + ReductionOp op_{}; ///< Binary operation reduction function object slot_allocator_type slot_allocator_{}; ///< Allocator used to allocate slots }; } // namespace cuco diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index d69d581fc..9d709a6c6 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -90,266 +90,7 @@ TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", using Key = T; using Value = T; - constexpr std::size_t num_keys{50'000'000}; - cuco::static_reduction_map map{100'000'000, -1, -1}; + constexpr std::size_t num_slots{50'000'000}; + cuco::static_reduction_map, Key, Value> map{num_slots, -1}; - auto m_view = map.get_device_mutable_view(); - auto view = map.get_device_view(); - - std::vector h_keys(num_keys); - std::vector h_values(num_keys); - std::vector> h_pairs(num_keys); - - generate_keys(h_keys.begin(), h_keys.end()); - - for (auto i = 0; i < num_keys; ++i) { - Key key = h_keys[i]; - Value val = h_keys[i]; - h_pairs[i].first = key; - h_pairs[i].second = val; - h_values[i] = val; - } - - thrust::device_vector d_keys(h_keys); - thrust::device_vector d_values(h_values); - thrust::device_vector> d_pairs(h_pairs); - thrust::device_vector d_results(num_keys); - thrust::device_vector d_contained(num_keys); - - // bulk function test cases - SECTION("All inserted keys-value pairs should be correctly recovered during find") - { - map.insert(d_pairs.begin(), d_pairs.end()); - map.find(d_keys.begin(), d_keys.end(), d_results.begin()); - auto zip = thrust::make_zip_iterator(thrust::make_tuple(d_results.begin(), d_values.begin())); - - REQUIRE(all_of(zip, zip + num_keys, [] __device__(auto const& p) { - return thrust::get<0>(p) == thrust::get<1>(p); - })); - } - - SECTION("All inserted keys-value pairs should be contained") - { - map.insert(d_pairs.begin(), d_pairs.end()); - map.contains(d_keys.begin(), d_keys.end(), d_contained.begin()); - - REQUIRE( - all_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); - } - - SECTION("Non-inserted keys-value pairs should not be contained") - { - map.contains(d_keys.begin(), d_keys.end(), d_contained.begin()); - - REQUIRE( - none_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); - } - - SECTION("Inserting unique keys should return insert success.") - { - if (Dist == dist_type::UNIQUE) { - REQUIRE(all_of(d_pairs.begin(), - d_pairs.end(), - [m_view] __device__(cuco::pair_type const& pair) mutable { - return m_view.insert(pair); - })); - } - } - - SECTION("Cannot find any key in an empty hash map with non-const view") - { - SECTION("non-const view") - { - REQUIRE(all_of(d_pairs.begin(), - d_pairs.end(), - [view] __device__(cuco::pair_type const& pair) mutable { - return view.find(pair.first) == view.end(); - })); - } - SECTION("const view") - { - REQUIRE(all_of( - d_pairs.begin(), d_pairs.end(), [view] __device__(cuco::pair_type const& pair) { - return view.find(pair.first) == view.end(); - })); - } - } - - SECTION("Keys are all found after inserting many keys.") - { - // Bulk insert keys - thrust::for_each(thrust::device, - d_pairs.begin(), - d_pairs.end(), - [m_view] __device__(cuco::pair_type const& pair) mutable { - m_view.insert(pair); - }); - - SECTION("non-const view") - { - // All keys should be found - REQUIRE(all_of(d_pairs.begin(), - d_pairs.end(), - [view] __device__(cuco::pair_type const& pair) mutable { - auto const found = view.find(pair.first); - return (found != view.end()) and (found->first.load() == pair.first and - found->second.load() == pair.second); - })); - } - SECTION("const view") - { - // All keys should be found - REQUIRE(all_of( - d_pairs.begin(), d_pairs.end(), [view] __device__(cuco::pair_type const& pair) { - auto const found = view.find(pair.first); - return (found != view.end()) and - (found->first.load() == pair.first and found->second.load() == pair.second); - })); - } - } } - -template -__global__ void shared_memory_test_kernel( - typename MapType::device_view const* const device_views, - typename MapType::device_view::key_type const* const insterted_keys, - typename MapType::device_view::mapped_type const* const inserted_values, - const size_t number_of_elements, - bool* const keys_exist, - bool* const keys_and_values_correct) -{ - // Each block processes one map - const size_t map_id = blockIdx.x; - const size_t offset = map_id * number_of_elements; - - __shared__ typename MapType::pair_atomic_type sm_buffer[CAPACITY]; - - auto g = cg::this_thread_block(); - typename MapType::device_view sm_device_view = - MapType::device_view::make_copy(g, sm_buffer, device_views[map_id]); - - for (int i = g.thread_rank(); i < number_of_elements; i += g.size()) { - auto found_pair_it = sm_device_view.find(insterted_keys[offset + i]); - - if (found_pair_it != sm_device_view.end()) { - keys_exist[offset + i] = true; - if (found_pair_it->first == insterted_keys[offset + i] and - found_pair_it->second == inserted_values[offset + i]) { - keys_and_values_correct[offset + i] = true; - } else { - keys_and_values_correct[offset + i] = false; - } - } else { - keys_exist[offset + i] = false; - keys_and_values_correct[offset + i] = true; - } - } -} - -TEMPLATE_TEST_CASE_SIG("Shared memory static map", - "", - ((typename T, dist_type Dist), T, Dist), - (int32_t, dist_type::UNIQUE), - (int64_t, dist_type::UNIQUE), - (int32_t, dist_type::UNIFORM), - (int64_t, dist_type::UNIFORM), - (int32_t, dist_type::GAUSSIAN), - (int64_t, dist_type::GAUSSIAN)) -{ - using KeyType = T; - using ValueType = T; - using MapType = cuco::static_reduction_map; - using DeviceViewType = typename MapType::device_view; - using DeviceViewIteratorType = typename DeviceViewType::iterator; - - constexpr std::size_t number_of_maps = 1000; - constexpr std::size_t elements_in_map = 500; - constexpr std::size_t map_capacity = 2 * elements_in_map; - - // one array for all maps, first elements_in_map element belong to map 0, second to map 1 and so - // on - std::vector h_keys(number_of_maps * elements_in_map); - std::vector h_values(number_of_maps * elements_in_map); - std::vector> h_pairs(number_of_maps * elements_in_map); - - // using std::unique_ptr because static_reduction_map does not have copy/move - // constructor/assignment operator yet - std::vector> maps; - - for (std::size_t map_id = 0; map_id < number_of_maps; ++map_id) { - const std::size_t offset = map_id * elements_in_map; - - generate_keys(h_keys.begin() + offset, - h_keys.begin() + offset + elements_in_map); - - for (std::size_t i = 0; i < elements_in_map; ++i) { - KeyType key = h_keys[offset + i]; - ValueType val = key < std::numeric_limits::max() ? key + 1 : 0; - h_values[offset + i] = val; - h_pairs[offset + i].first = key; - h_pairs[offset + i].second = val; - } - - maps.push_back(std::make_unique(map_capacity, -1, -1)); - } - - thrust::device_vector d_keys(h_keys); - thrust::device_vector d_values(h_values); - thrust::device_vector> d_pairs(h_pairs); - - SECTION("Keys are all found after insertion.") - { - std::vector h_device_views; - for (std::size_t map_id = 0; map_id < number_of_maps; ++map_id) { - const std::size_t offset = map_id * elements_in_map; - - MapType* map = maps[map_id].get(); - map->insert(d_pairs.begin() + offset, d_pairs.begin() + offset + elements_in_map); - h_device_views.push_back(map->get_device_view()); - } - thrust::device_vector d_device_views(h_device_views); - - thrust::device_vector d_keys_exist(number_of_maps * elements_in_map); - thrust::device_vector d_keys_and_values_correct(number_of_maps * elements_in_map); - - shared_memory_test_kernel - <<>>(d_device_views.data().get(), - d_keys.data().get(), - d_values.data().get(), - elements_in_map, - d_keys_exist.data().get(), - d_keys_and_values_correct.data().get()); - - REQUIRE(d_keys_exist.size() == d_keys_and_values_correct.size()); - auto zip = thrust::make_zip_iterator( - thrust::make_tuple(d_keys_exist.begin(), d_keys_and_values_correct.begin())); - - REQUIRE(all_of(zip, zip + d_keys_exist.size(), [] __device__(auto const& z) { - return thrust::get<0>(z) and thrust::get<1>(z); - })); - } - - SECTION("No key is found before insertion.") - { - std::vector h_device_views; - for (std::size_t map_id = 0; map_id < number_of_maps; ++map_id) { - h_device_views.push_back(maps[map_id].get()->get_device_view()); - } - thrust::device_vector d_device_views(h_device_views); - - thrust::device_vector d_keys_exist(number_of_maps * elements_in_map); - thrust::device_vector d_keys_and_values_correct(number_of_maps * elements_in_map); - - shared_memory_test_kernel - <<>>(d_device_views.data().get(), - d_keys.data().get(), - d_values.data().get(), - elements_in_map, - d_keys_exist.data().get(), - d_keys_and_values_correct.data().get()); - - REQUIRE(none_of(d_keys_exist.begin(), d_keys_exist.end(), [] __device__(const bool key_found) { - return key_found; - })); - } -} \ No newline at end of file From fd3b98f981d5742ea0dd98c3faa10e7eb7d6bb15 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Mon, 4 Jan 2021 15:19:33 -0600 Subject: [PATCH 03/69] Fix static_assert for ReductionOp::value_type. --- include/cuco/static_reduction_map.cuh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index d66c6cf4a..a33de7026 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -123,6 +123,8 @@ template > class static_reduction_map { static_assert(std::is_arithmetic::value, "Unsupported, non-arithmetic key type."); + static_assert(std::is_same::value, + "Type mismatch between ReductionOp::value_type and Value"); public: using value_type = cuco::pair_type; From a3678fbd9417787f0c7818a992d4d1e6284eace6 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Mon, 4 Jan 2021 21:49:02 -0600 Subject: [PATCH 04/69] CG reduction insert implementation. --- include/cuco/detail/static_reduction_map.inl | 106 ++++++++----------- include/cuco/static_reduction_map.cuh | 2 +- 2 files changed, 46 insertions(+), 62 deletions(-) diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index be28e0f28..140c728a4 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -156,7 +156,7 @@ static_reduction_map::device_mutable_ slot_key.compare_exchange_strong(expected_key, insert_pair.first, memory_order_relaxed); if (key_success or key_equal(insert_pair.first, expected_key)) { - // return do_op{}(slot_value, insert_pair.second); + return op_.apply(slot_value, insert_pair.second); } // if we couldn't insert the key, but it wasn't a duplicate, then there must @@ -171,77 +171,61 @@ template template -__device__ bool +__device__ void static_reduction_map::device_mutable_view::insert( CG g, value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(g, insert_pair.first, hash); + auto& slot_key = current_slot->first; + auto& slot_value = current_slot->second; while (true) { - key_type const existing_key = current_slot->first; + auto const current_key = slot_key.load(cuda::std::memory_order_relaxed); - // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as the - // sentinel is not a valid key value. Therefore, first check for the sentinel - auto const slot_is_empty = (existing_key == this->get_empty_key_sentinel()); + // The user provided `key_equal` should never be used to compare against `empty_key_sentinel` as + // the sentinel is not a valid key value. Therefore, first check for the sentinel + // TODO: Use memcmp + auto const slot_is_empty = (current_key == this->get_empty_key_sentinel()); - // the key we are trying to insert is already in the map, so we return with failure to insert - if (g.ballot(not slot_is_empty and key_equal(existing_key, insert_pair.first))) { - return false; - } + auto const key_exists = not slot_is_empty and key_equal(current_key, insert_pair.first); - auto const window_contains_empty = g.ballot(slot_is_empty); + // Key already exists, aggregate with it's value + if (key_exists) { op_.apply(slot_value, insert_pair.second); } - // we found an empty slot, but not the key we are inserting, so this must - // be an empty slot into which we can insert the key - if (window_contains_empty) { + // If key already exists in the CG window, all threads exit + if (g.ballot(key_exists)) { return; } + + auto const window_empty_mask = g.ballot(slot_is_empty); + + if (window_empty_mask) { // the first lane in the group with an empty slot will attempt the insert - insert_result status{insert_result::CONTINUE}; - uint32_t src_lane = __ffs(window_contains_empty) - 1; - - if (g.thread_rank() == src_lane) { - using cuda::std::memory_order_relaxed; - auto expected_key = this->get_empty_key_sentinel(); - auto expected_value = this->get_empty_value_sentinel(); - auto& slot_key = current_slot->first; - auto& slot_value = current_slot->second; - - bool key_success = - slot_key.compare_exchange_strong(expected_key, insert_pair.first, memory_order_relaxed); - bool value_success = slot_value.compare_exchange_strong( - expected_value, insert_pair.second, memory_order_relaxed); - - if (key_success) { - while (not value_success) { - value_success = - slot_value.compare_exchange_strong(expected_value = this->get_empty_value_sentinel(), - insert_pair.second, - memory_order_relaxed); + auto const src_lane = __ffs(window_empty_mask) - 1; + + auto const thread_success = [&]() { + if (g.thread_rank() == src_lane) { + auto expected_key = this->get_empty_key_sentinel(); + + auto const key_success = slot_key.compare_exchange_strong( + expected_key, insert_pair.first, cuda::memory_order_relaxed); + + if (key_success or key_equal(insert_pair.first, expected_key)) { + op_.apply(slot_value, insert_pair.second); + return true; } - status = insert_result::SUCCESS; - } else if (value_success) { - slot_value.store(this->get_empty_value_sentinel(), memory_order_relaxed); } + return false; + }(); - // our key was already present in the slot, so our key is a duplicate - if (key_equal(insert_pair.first, expected_key)) { status = insert_result::DUPLICATE; } - // another key was inserted in the slot we wanted to try - // so we need to try the next empty slot in the window - } + auto const src_success = g.shfl(thread_success, src_lane); - uint32_t res_status = g.shfl(static_cast(status), src_lane); - status = static_cast(res_status); + if (src_success) { return; } - // successful insert - if (status == insert_result::SUCCESS) { return true; } - // duplicate present during insert - if (status == insert_result::DUPLICATE) { return false; } // if we've gotten this far, a different key took our spot // before we could insert. We need to retry the insert on the // same window - } - // if there are no empty slots in the current window, - // we move onto the next window - else { + } else { + // if there are no empty slots in the current window, + // we move onto the next window current_slot = next_slot(g, current_slot); } } @@ -313,8 +297,8 @@ __device__ while (true) { auto const existing_key = current_slot->first.load(cuda::std::memory_order_relaxed); - // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as the - // sentinel is not a valid key value. Therefore, first check for the sentinel + // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as + // the sentinel is not a valid key value. Therefore, first check for the sentinel auto const slot_is_empty = (existing_key == this->get_empty_key_sentinel()); // the key we were searching for was found by one of the threads, @@ -353,8 +337,8 @@ __device__ typename static_reduction_mapfirst.load(cuda::std::memory_order_relaxed); - // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as the - // sentinel is not a valid key value. Therefore, first check for the sentinel + // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as + // the sentinel is not a valid key value. Therefore, first check for the sentinel auto const slot_is_empty = (existing_key == this->get_empty_key_sentinel()); // the key we were searching for was found by one of the threads, so we return an iterator to @@ -417,8 +401,8 @@ static_reduction_map::device_view::co while (true) { key_type const existing_key = current_slot->first.load(cuda::std::memory_order_relaxed); - // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as the - // sentinel is not a valid key value. Therefore, first check for the sentinel + // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as + // the sentinel is not a valid key value. Therefore, first check for the sentinel auto const slot_is_empty = (existing_key == this->get_empty_key_sentinel()); // the key we were searching for was found by one of the threads, so we return an iterator to @@ -428,8 +412,8 @@ static_reduction_map::device_view::co // we found an empty slot, meaning that the key we're searching for isn't present if (g.ballot(slot_is_empty)) { return false; } - // otherwise, all slots in the current window are full with other keys, so we move onto the next - // window + // otherwise, all slots in the current window are full with other keys, so we move onto the + // next window current_slot = next_slot(g, current_slot); } } diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index a33de7026..47b21d6f2 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -600,7 +600,7 @@ class static_reduction_map { template , typename KeyEqual = thrust::equal_to> - __device__ bool insert(CG g, + __device__ void insert(CG g, value_type const& insert_pair, Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}) noexcept; From 5a65bf61674077971dd0a65bfe870f75bcfeb1da Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Tue, 5 Jan 2021 09:02:51 -0600 Subject: [PATCH 05/69] Cleanup of CG insert. --- include/cuco/detail/static_reduction_map.inl | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index 140c728a4..d833d2650 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -201,7 +201,7 @@ static_reduction_map::device_mutable_ // the first lane in the group with an empty slot will attempt the insert auto const src_lane = __ffs(window_empty_mask) - 1; - auto const thread_success = [&]() { + auto const update_success = [&]() { if (g.thread_rank() == src_lane) { auto expected_key = this->get_empty_key_sentinel(); @@ -216,16 +216,12 @@ static_reduction_map::device_mutable_ return false; }(); - auto const src_success = g.shfl(thread_success, src_lane); + // If the update succeeded, the thread group exits + if (g.shfl(update_success, src_lane)) { return; } - if (src_success) { return; } - - // if we've gotten this far, a different key took our spot - // before we could insert. We need to retry the insert on the - // same window + // A different key took the current slot. Look for an empty slot in the current window } else { - // if there are no empty slots in the current window, - // we move onto the next window + // No empty slots in the current window, move onto the next window current_slot = next_slot(g, current_slot); } } From 28e09953cd2ceb1fadf6b8ca93aa379624887b9d Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Tue, 5 Jan 2021 14:49:16 -0600 Subject: [PATCH 06/69] Pass reduction op to device view ctors. --- include/cuco/static_reduction_map.cuh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 47b21d6f2..1a94b8270 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -900,7 +900,7 @@ class static_reduction_map { */ device_view get_device_view() const noexcept { - return device_view(slots_, capacity_, empty_key_sentinel_, empty_value_sentinel_); + return device_view(slots_, capacity_, empty_key_sentinel_, op_); } /** @@ -911,7 +911,7 @@ class static_reduction_map { */ device_mutable_view get_device_mutable_view() const noexcept { - return device_mutable_view(slots_, capacity_, empty_key_sentinel_, empty_value_sentinel_); + return device_mutable_view(slots_, capacity_, empty_key_sentinel_, op_); } private: From 8dc64ee9f4f06392e3c526a2507c2b8d7f1dd8dd Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Tue, 5 Jan 2021 14:50:19 -0600 Subject: [PATCH 07/69] Add pair ctor for constructing from two elements. --- include/cuco/detail/pair.cuh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/cuco/detail/pair.cuh b/include/cuco/detail/pair.cuh index 8bc6ec6b4..8ed10b32b 100644 --- a/include/cuco/detail/pair.cuh +++ b/include/cuco/detail/pair.cuh @@ -65,6 +65,10 @@ struct alignas(detail::pair_alignment()) pair { : first{p.first}, second{p.second} { } + __host__ __device__ constexpr pair(First const& f, Second const& s) noexcept + : first{f}, second{s} + { + } }; template From 573bce28f00b2fd57749537d684f39ad09d08148 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Tue, 5 Jan 2021 14:50:38 -0600 Subject: [PATCH 08/69] Allow bulk insert kernel to work on iterators over tuples. --- include/cuco/detail/static_reduction_map_kernels.cuh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/include/cuco/detail/static_reduction_map_kernels.cuh b/include/cuco/detail/static_reduction_map_kernels.cuh index 6ded5e99d..9849efb44 100644 --- a/include/cuco/detail/static_reduction_map_kernels.cuh +++ b/include/cuco/detail/static_reduction_map_kernels.cuh @@ -142,7 +142,10 @@ __global__ void insert( while (it < last) { // force conversion to value_type - typename viewT::value_type const insert_pair{*it}; + typename viewT::value_type const insert_pair{ + static_cast(thrust::get<0>(*it)), + static_cast(thrust::get<1>(*it))}; + if (view.insert(tile, insert_pair, hash, key_equal) && tile.thread_rank() == 0) { thread_num_successes++; } From d9236e588e1eb87051b891de707b1326624c8795 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Tue, 5 Jan 2021 15:22:16 -0600 Subject: [PATCH 09/69] Add device decorator to reduction op definition. --- include/cuco/static_reduction_map.cuh | 1 + 1 file changed, 1 insertion(+) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 1a94b8270..bac8132ae 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -44,6 +44,7 @@ struct reduce_add { static constexpr T identity = 0; template + __device__ T apply(cuda::atomic& slot, T2 const& value) { return slot.fetch_add(value); From 89ed44e656e4ab16981127d4d7449788b1c390e1 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Tue, 5 Jan 2021 15:22:48 -0600 Subject: [PATCH 10/69] Add get_op function to allow accessing the op from the derived types. --- include/cuco/static_reduction_map.cuh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index bac8132ae..5f2f0341d 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -283,6 +283,12 @@ class static_reduction_map { { } + /** + * @brief Gets the binary op + * + */ + __device__ ReductionOp get_op() const { return op_; } + /** * @brief Gets slots array. * From e28db800db2afca8df262592f68e1a698cfb12e3 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Tue, 5 Jan 2021 15:23:18 -0600 Subject: [PATCH 11/69] Make insert return a bool after all. We need to return a bool so we can keep track of how many unique keys were inserted in a bulk insert. --- include/cuco/static_reduction_map.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 5f2f0341d..c8e2ecc13 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -607,7 +607,7 @@ class static_reduction_map { template , typename KeyEqual = thrust::equal_to> - __device__ void insert(CG g, + __device__ bool insert(CG g, value_type const& insert_pair, Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}) noexcept; From 0eeac206df5acfaf869bd611c66216230dc63fb8 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Tue, 5 Jan 2021 15:23:45 -0600 Subject: [PATCH 12/69] Use get_op in implementation. --- include/cuco/detail/static_reduction_map.inl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index d833d2650..cf98e46b3 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -156,7 +156,7 @@ static_reduction_map::device_mutable_ slot_key.compare_exchange_strong(expected_key, insert_pair.first, memory_order_relaxed); if (key_success or key_equal(insert_pair.first, expected_key)) { - return op_.apply(slot_value, insert_pair.second); + return this->get_op().apply(slot_value, insert_pair.second); } // if we couldn't insert the key, but it wasn't a duplicate, then there must @@ -190,7 +190,7 @@ static_reduction_map::device_mutable_ auto const key_exists = not slot_is_empty and key_equal(current_key, insert_pair.first); // Key already exists, aggregate with it's value - if (key_exists) { op_.apply(slot_value, insert_pair.second); } + if (key_exists) { this->get_op().apply(slot_value, insert_pair.second); } // If key already exists in the CG window, all threads exit if (g.ballot(key_exists)) { return; } @@ -209,7 +209,7 @@ static_reduction_map::device_mutable_ expected_key, insert_pair.first, cuda::memory_order_relaxed); if (key_success or key_equal(insert_pair.first, expected_key)) { - op_.apply(slot_value, insert_pair.second); + this->get_op().apply(slot_value, insert_pair.second); return true; } } From fa31c8117abe243ff9ac55c9edda8ac69719ceef Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Tue, 5 Jan 2021 15:24:02 -0600 Subject: [PATCH 13/69] Make insert return a bool. --- include/cuco/detail/static_reduction_map.inl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index cf98e46b3..2c1e434aa 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -171,7 +171,7 @@ template template -__device__ void +__device__ bool static_reduction_map::device_mutable_view::insert( CG g, value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept { @@ -193,7 +193,7 @@ static_reduction_map::device_mutable_ if (key_exists) { this->get_op().apply(slot_value, insert_pair.second); } // If key already exists in the CG window, all threads exit - if (g.ballot(key_exists)) { return; } + if (g.ballot(key_exists)) { return false; } auto const window_empty_mask = g.ballot(slot_is_empty); @@ -217,7 +217,7 @@ static_reduction_map::device_mutable_ }(); // If the update succeeded, the thread group exits - if (g.shfl(update_success, src_lane)) { return; } + if (g.shfl(update_success, src_lane)) { return true; } // A different key took the current slot. Look for an empty slot in the current window } else { From ab81b2b7d38ab4d414681b35b9b905685738f9d9 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Tue, 5 Jan 2021 16:07:52 -0600 Subject: [PATCH 14/69] Correct insert to return if the key was the first key inserted. --- include/cuco/detail/static_reduction_map.inl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index 2c1e434aa..9025ceefa 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -190,7 +190,9 @@ static_reduction_map::device_mutable_ auto const key_exists = not slot_is_empty and key_equal(current_key, insert_pair.first); // Key already exists, aggregate with it's value - if (key_exists) { this->get_op().apply(slot_value, insert_pair.second); } + if (key_exists) { + this->get_op().apply(slot_value, insert_pair.second); + } // If key already exists in the CG window, all threads exit if (g.ballot(key_exists)) { return false; } @@ -210,14 +212,16 @@ static_reduction_map::device_mutable_ if (key_success or key_equal(insert_pair.first, expected_key)) { this->get_op().apply(slot_value, insert_pair.second); - return true; + return key_success; } } return false; }(); // If the update succeeded, the thread group exits - if (g.shfl(update_success, src_lane)) { return true; } + if (g.shfl(update_success, src_lane)) { + return true; + } // A different key took the current slot. Look for an empty slot in the current window } else { From 46f9b73794a28cef4b4dfc7dd5489e0966741b7f Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Tue, 5 Jan 2021 16:08:02 -0600 Subject: [PATCH 15/69] First test verifying size passed. --- tests/static_reduction_map/static_reduction_map_test.cu | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index 9d709a6c6..958571129 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -93,4 +93,12 @@ TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", constexpr std::size_t num_slots{50'000'000}; cuco::static_reduction_map, Key, Value> map{num_slots, -1}; + SECTION("Inserting all the same key should sum all of their corresponding values") { + thrust::device_vector keys(100, 42); + thrust::device_vector values(keys.size(), 1); + auto zip = thrust::make_zip_iterator(thrust::make_tuple(keys.begin(), values.begin())); + auto zip_end = zip + keys.size(); + map.insert(zip, zip_end); + REQUIRE(map.get_size() == 1); + } } From 8aebabbbdf79a307d61f9e3a085fbd060362e5e2 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Wed, 6 Jan 2021 13:01:49 -0600 Subject: [PATCH 16/69] Update CG insert logic. The mapped value is updated in the case of a new insert or updating an existing key, but we need to track if the insert was the first time that key was inserted. --- include/cuco/detail/static_reduction_map.inl | 33 ++++++++++---------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index 9025ceefa..f5e26ac58 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -190,9 +190,7 @@ static_reduction_map::device_mutable_ auto const key_exists = not slot_is_empty and key_equal(current_key, insert_pair.first); // Key already exists, aggregate with it's value - if (key_exists) { - this->get_op().apply(slot_value, insert_pair.second); - } + if (key_exists) { this->get_op().apply(slot_value, insert_pair.second); } // If key already exists in the CG window, all threads exit if (g.ballot(key_exists)) { return false; } @@ -203,24 +201,27 @@ static_reduction_map::device_mutable_ // the first lane in the group with an empty slot will attempt the insert auto const src_lane = __ffs(window_empty_mask) - 1; - auto const update_success = [&]() { - if (g.thread_rank() == src_lane) { - auto expected_key = this->get_empty_key_sentinel(); + auto const attempt_update = [&]() { + auto expected_key = this->get_empty_key_sentinel(); - auto const key_success = slot_key.compare_exchange_strong( - expected_key, insert_pair.first, cuda::memory_order_relaxed); + auto const key_success = slot_key.compare_exchange_strong( + expected_key, insert_pair.first, cuda::memory_order_relaxed); - if (key_success or key_equal(insert_pair.first, expected_key)) { - this->get_op().apply(slot_value, insert_pair.second); - return key_success; - } + if (key_success or key_equal(insert_pair.first, expected_key)) { + this->get_op().apply(slot_value, insert_pair.second); + return key_success ? insert_result::SUCCESS : insert_result::DUPLICATE; } - return false; - }(); + return insert_result::CONTINUE; + }; + + auto const update_result = + (g.thread_rank() == src_lane) ? attempt_update() : insert_result::CONTINUE; + + auto const window_result = g.shfl(update_result, src_lane); // If the update succeeded, the thread group exits - if (g.shfl(update_success, src_lane)) { - return true; + if (window_result != insert_result::CONTINUE) { + return (window_result == insert_result::SUCCESS); } // A different key took the current slot. Look for an empty slot in the current window From 9fb930ec216d6c2a074eb7d537a2572c5686a585 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Wed, 6 Jan 2021 13:01:57 -0600 Subject: [PATCH 17/69] Add more tests. --- .../static_reduction_map_test.cu | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index 958571129..144c02ec8 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -80,25 +80,41 @@ static void generate_keys(OutputIt output_begin, OutputIt output_end) TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", "", ((typename T, dist_type Dist), T, Dist), - (int32_t, dist_type::UNIQUE), - (int64_t, dist_type::UNIQUE), - (int32_t, dist_type::UNIFORM), - (int64_t, dist_type::UNIFORM), - (int32_t, dist_type::GAUSSIAN), - (int64_t, dist_type::GAUSSIAN)) + (int32_t, dist_type::UNIQUE)) { using Key = T; using Value = T; - constexpr std::size_t num_slots{50'000'000}; + constexpr std::size_t num_slots{200}; cuco::static_reduction_map, Key, Value> map{num_slots, -1}; - SECTION("Inserting all the same key should sum all of their corresponding values") { - thrust::device_vector keys(100, 42); - thrust::device_vector values(keys.size(), 1); - auto zip = thrust::make_zip_iterator(thrust::make_tuple(keys.begin(), values.begin())); - auto zip_end = zip + keys.size(); - map.insert(zip, zip_end); - REQUIRE(map.get_size() == 1); + SECTION("Inserting identical keys") + { + thrust::device_vector keys(100, 42); + thrust::device_vector values(keys.size(), 1); + auto zip = thrust::make_zip_iterator(thrust::make_tuple(keys.begin(), values.begin())); + auto zip_end = zip + keys.size(); + map.insert(zip, zip_end); + + SECTION("There should only be one key in the map") { REQUIRE(map.get_size() == 1); } + + SECTION("Map should contain the inserted key") + { + thrust::device_vector contained(keys.size()); + map.contains(keys.begin(), keys.end(), contained.begin()); + REQUIRE(all_of(contained.begin(), contained.end(), [] __device__(bool c) { return c; })); + } + + SECTION("Found value should equal aggregate of inserted values") + { + thrust::device_vector found(keys.size()); + map.find(keys.begin(), keys.end(), found.begin()); + auto const expected_aggregate = keys.size(); // All keys inserted "1", so the + // sum aggregate should be + // equal to the number of keys inserted + REQUIRE(all_of(found.begin(), found.end(), [expected_aggregate] __device__(Value v) { + return v == expected_aggregate; + })); + } } } From 24261b2a9b4e327409a26869804dea8abc78a993 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 7 Jan 2021 11:55:12 -0600 Subject: [PATCH 18/69] Add test for inserting all unique keys. --- .../static_reduction_map_test.cu | 113 +++++++++--------- 1 file changed, 55 insertions(+), 58 deletions(-) diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index 144c02ec8..084b24563 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -23,8 +23,6 @@ #include namespace { -namespace cg = cooperative_groups; - // Thrust logical algorithms (any_of/all_of/none_of) don't work with device // lambdas: See https://github.com/thrust/thrust/issues/1062 template @@ -47,74 +45,73 @@ bool none_of(Iterator begin, Iterator end, Predicate p) } } // namespace -enum class dist_type { UNIQUE, UNIFORM, GAUSSIAN }; - -template -static void generate_keys(OutputIt output_begin, OutputIt output_end) +TEMPLATE_TEST_CASE_SIG("Insert all identical keys", + "", + ((typename Key, typename Value), Key, Value), + (int32_t, int32_t)) { - auto num_keys = std::distance(output_begin, output_end); + constexpr std::size_t num_slots{200}; + cuco::static_reduction_map, Key, Value> map{num_slots, -1}; - std::random_device rd; - std::mt19937 gen{rd()}; + thrust::device_vector keys(100, 42); + thrust::device_vector values(keys.size(), 1); + auto zip = thrust::make_zip_iterator(thrust::make_tuple(keys.begin(), values.begin())); + auto zip_end = zip + keys.size(); + map.insert(zip, zip_end); + + SECTION("There should only be one key in the map") { REQUIRE(map.get_size() == 1); } + + SECTION("Map should contain the inserted key") + { + thrust::device_vector contained(keys.size()); + map.contains(keys.begin(), keys.end(), contained.begin()); + REQUIRE(all_of(contained.begin(), contained.end(), [] __device__(bool c) { return c; })); + } - switch (Dist) { - case dist_type::UNIQUE: - for (auto i = 0; i < num_keys; ++i) { - output_begin[i] = i; - } - break; - case dist_type::UNIFORM: - for (auto i = 0; i < num_keys; ++i) { - output_begin[i] = std::abs(static_cast(gen())); - } - break; - case dist_type::GAUSSIAN: - std::normal_distribution<> dg{1e9, 1e7}; - for (auto i = 0; i < num_keys; ++i) { - output_begin[i] = std::abs(static_cast(dg(gen))); - } - break; + SECTION("Found value should equal aggregate of inserted values") + { + thrust::device_vector found(keys.size()); + map.find(keys.begin(), keys.end(), found.begin()); + auto const expected_aggregate = keys.size(); // All keys inserted "1", so the + // sum aggregate should be + // equal to the number of keys inserted + REQUIRE(all_of(found.begin(), found.end(), [expected_aggregate] __device__(Value v) { + return v == expected_aggregate; + })); } } -TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", +TEMPLATE_TEST_CASE_SIG("Insert all unique keys", "", - ((typename T, dist_type Dist), T, Dist), - (int32_t, dist_type::UNIQUE)) + ((typename Key, typename Value), Key, Value), + (int32_t, int32_t)) { - using Key = T; - using Value = T; - - constexpr std::size_t num_slots{200}; + constexpr std::size_t num_keys = 100; + constexpr std::size_t num_slots{num_keys * 3}; cuco::static_reduction_map, Key, Value> map{num_slots, -1}; - SECTION("Inserting identical keys") - { - thrust::device_vector keys(100, 42); - thrust::device_vector values(keys.size(), 1); - auto zip = thrust::make_zip_iterator(thrust::make_tuple(keys.begin(), values.begin())); - auto zip_end = zip + keys.size(); - map.insert(zip, zip_end); + auto keys_begin = thrust::make_counting_iterator(0); + auto values_begin = thrust::make_counting_iterator(0); + auto zip = thrust::make_zip_iterator(thrust::make_tuple(keys_begin, values_begin)); + auto zip_end = zip + num_keys; + map.insert(zip, zip_end); - SECTION("There should only be one key in the map") { REQUIRE(map.get_size() == 1); } + SECTION("Size of map should equal number of inserted keys") + { + REQUIRE(map.get_size() == num_keys); + } - SECTION("Map should contain the inserted key") - { - thrust::device_vector contained(keys.size()); - map.contains(keys.begin(), keys.end(), contained.begin()); - REQUIRE(all_of(contained.begin(), contained.end(), [] __device__(bool c) { return c; })); - } + SECTION("Map should contain the inserted keys") + { + thrust::device_vector contained(num_keys); + map.contains(keys_begin, keys_begin + num_keys, contained.begin()); + REQUIRE(all_of(contained.begin(), contained.end(), [] __device__(bool c) { return c; })); + } - SECTION("Found value should equal aggregate of inserted values") - { - thrust::device_vector found(keys.size()); - map.find(keys.begin(), keys.end(), found.begin()); - auto const expected_aggregate = keys.size(); // All keys inserted "1", so the - // sum aggregate should be - // equal to the number of keys inserted - REQUIRE(all_of(found.begin(), found.end(), [expected_aggregate] __device__(Value v) { - return v == expected_aggregate; - })); - } + SECTION("Found value should equal inserted value") + { + thrust::device_vector found(num_keys); + map.find(keys_begin, keys_begin + num_keys, found.begin()); + REQUIRE(thrust::equal(thrust::device, values_begin, values_begin + num_keys, found.begin())); } } From e635e3179d4f9d8186e033acbe21d8c5275b213c Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 7 Jan 2021 11:55:19 -0600 Subject: [PATCH 19/69] Use relaxed fetch_add. --- include/cuco/static_reduction_map.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index c8e2ecc13..34fa2cba0 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -47,7 +47,7 @@ struct reduce_add { __device__ T apply(cuda::atomic& slot, T2 const& value) { - return slot.fetch_add(value); + return slot.fetch_add(value, cuda::memory_order_relaxed); } }; From d749445c6b967384aaf23e27b419ebcab26883b1 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 7 Jan 2021 13:07:19 -0600 Subject: [PATCH 20/69] Update the slot references each iteration. --- include/cuco/detail/static_reduction_map.inl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index f5e26ac58..86b500e4e 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -176,10 +176,10 @@ static_reduction_map::device_mutable_ CG g, value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(g, insert_pair.first, hash); - auto& slot_key = current_slot->first; - auto& slot_value = current_slot->second; while (true) { + auto& slot_key = current_slot->first; + auto& slot_value = current_slot->second; auto const current_key = slot_key.load(cuda::std::memory_order_relaxed); // The user provided `key_equal` should never be used to compare against `empty_key_sentinel` as From ca9f7d6b4362cad46521e3f9bce1b6b9926f2345 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 7 Jan 2021 13:07:30 -0600 Subject: [PATCH 21/69] Increase size of unique key test. --- tests/static_reduction_map/static_reduction_map_test.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index 084b24563..9a61b8b4d 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -86,8 +86,8 @@ TEMPLATE_TEST_CASE_SIG("Insert all unique keys", ((typename Key, typename Value), Key, Value), (int32_t, int32_t)) { - constexpr std::size_t num_keys = 100; - constexpr std::size_t num_slots{num_keys * 3}; + constexpr std::size_t num_keys = 10000; + constexpr std::size_t num_slots{num_keys * 2}; cuco::static_reduction_map, Key, Value> map{num_slots, -1}; auto keys_begin = thrust::make_counting_iterator(0); From 9eebd172295416e1bb3a598b95da6418450ff19b Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 7 Jan 2021 14:33:20 -0600 Subject: [PATCH 22/69] Make map size function of number of keys. --- tests/static_reduction_map/static_reduction_map_test.cu | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index 9a61b8b4d..bb57f0847 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -50,11 +50,12 @@ TEMPLATE_TEST_CASE_SIG("Insert all identical keys", ((typename Key, typename Value), Key, Value), (int32_t, int32_t)) { - constexpr std::size_t num_slots{200}; - cuco::static_reduction_map, Key, Value> map{num_slots, -1}; - thrust::device_vector keys(100, 42); thrust::device_vector values(keys.size(), 1); + + auto const num_slots{keys.size() * 2}; + cuco::static_reduction_map, Key, Value> map{num_slots, -1}; + auto zip = thrust::make_zip_iterator(thrust::make_tuple(keys.begin(), values.begin())); auto zip_end = zip + keys.size(); map.insert(zip, zip_end); From 212b8f6dbc212d11d48e9aee6d14cfd24bd83927 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 7 Jan 2021 16:51:18 -0600 Subject: [PATCH 23/69] Add other agg ops. --- include/cuco/static_reduction_map.cuh | 41 ++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 34fa2cba0..470589724 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -45,12 +45,51 @@ struct reduce_add { template __device__ - T apply(cuda::atomic& slot, T2 const& value) + T apply(cuda::atomic& slot, T2 const& value) const { return slot.fetch_add(value, cuda::memory_order_relaxed); } }; +template +struct reduce_sub { + using value_type = T; + static constexpr T identity = 0; + + template + __device__ + T apply(cuda::atomic& slot, T2 const& value) const + { + return slot.fetch_sub(value, cuda::memory_order_relaxed); + } +}; + +template +struct reduce_min { + using value_type = T; + static constexpr T identity = std::numeric_limits::max(); + + template + __device__ + T apply(cuda::atomic& slot, T2 const& value) const + { + return slot.fetch_min(value, cuda::memory_order_relaxed); + } +}; + +template +struct reduce_max { + using value_type = T; + static constexpr T identity = std::numeric_limits::lowest(); + + template + __device__ + T apply(cuda::atomic& slot, T2 const& value) const + { + return slot.fetch_max(value, cuda::memory_order_relaxed); + } +}; + /** * @brief A GPU-accelerated, unordered, associative container of key-value * pairs with unique keys. From cda527a52043c5e34115d71c854851fcbb97a542 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 7 Jan 2021 17:01:12 -0600 Subject: [PATCH 24/69] Add custom binary op. --- include/cuco/static_reduction_map.cuh | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 470589724..c8dca31b0 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -44,8 +44,7 @@ struct reduce_add { static constexpr T identity = 0; template - __device__ - T apply(cuda::atomic& slot, T2 const& value) const + __device__ T apply(cuda::atomic& slot, T2 const& value) const { return slot.fetch_add(value, cuda::memory_order_relaxed); } @@ -57,8 +56,7 @@ struct reduce_sub { static constexpr T identity = 0; template - __device__ - T apply(cuda::atomic& slot, T2 const& value) const + __device__ T apply(cuda::atomic& slot, T2 const& value) const { return slot.fetch_sub(value, cuda::memory_order_relaxed); } @@ -70,8 +68,7 @@ struct reduce_min { static constexpr T identity = std::numeric_limits::max(); template - __device__ - T apply(cuda::atomic& slot, T2 const& value) const + __device__ T apply(cuda::atomic& slot, T2 const& value) const { return slot.fetch_min(value, cuda::memory_order_relaxed); } @@ -83,13 +80,27 @@ struct reduce_max { static constexpr T identity = std::numeric_limits::lowest(); template - __device__ - T apply(cuda::atomic& slot, T2 const& value) const + __device__ T apply(cuda::atomic& slot, T2 const& value) const { return slot.fetch_max(value, cuda::memory_order_relaxed); } }; +template +struct custom_op { + using value_type = T; + static constexpr T identity = Identity; + + Op op; + + template + __device__ T apply(cuda::atomic& slot, T2 const& value) const + { + auto old = slot.load(cuda::memory_order_relaxed); + while (not slot.compare_exchange_strong(old, op(old, value), cuda::memory_order_relaxed)) {} + } +}; + /** * @brief A GPU-accelerated, unordered, associative container of key-value * pairs with unique keys. From 7c1af0f4f42f3565b16dd17aefcd194568c38624 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 7 Jan 2021 17:17:00 -0600 Subject: [PATCH 25/69] Return old value in custom op. --- include/cuco/static_reduction_map.cuh | 1 + 1 file changed, 1 insertion(+) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index c8dca31b0..b13c1f5af 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -98,6 +98,7 @@ struct custom_op { { auto old = slot.load(cuda::memory_order_relaxed); while (not slot.compare_exchange_strong(old, op(old, value), cuda::memory_order_relaxed)) {} + return old; } }; From 3f1b59d9362f0ad517199c539c947d5e32692f81 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 8 Apr 2021 09:55:38 -0500 Subject: [PATCH 26/69] reduction map benchmarks. --- benchmarks/CMakeLists.txt | 7 +- benchmarks/hash_table/static_map_bench.cu | 132 +++++++++--------- .../hash_table/static_reduction_map_bench.cu | 130 +++++++++++++++++ examples/CMakeLists.txt | 2 +- tests/CMakeLists.txt | 2 +- 5 files changed, 206 insertions(+), 67 deletions(-) create mode 100644 benchmarks/hash_table/static_reduction_map_bench.cu diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 45b02848d..f9464a6eb 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -28,6 +28,8 @@ if("${GPU_ARCHS}" STREQUAL "") evaluate_gpu_archs(GPU_ARCHS) endif() +message("GPU_ARCHS = ${GPU_ARCHS}") + ################################################################################################### # - compiler function ----------------------------------------------------------------------------- @@ -35,7 +37,7 @@ function(ConfigureBench BENCH_NAME BENCH_SRC) add_executable(${BENCH_NAME} "${BENCH_SRC}") set_target_properties(${BENCH_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON - CUDA_ARCHITECTURES ${GPU_ARCHS} + CUDA_ARCHITECTURES "${GPU_ARCHS}" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/gbenchmarks") target_include_directories(${BENCH_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") @@ -58,6 +60,9 @@ ConfigureBench(DYNAMIC_MAP_BENCH "${DYNAMIC_MAP_BENCH_SRC}") set(STATIC_MAP_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/hash_table/static_map_bench.cu") ConfigureBench(STATIC_MAP_BENCH "${STATIC_MAP_BENCH_SRC}") +################################################################################################### +ConfigureBench(STATIC_REDUCTION_MAP_BENCH "${CMAKE_CURRENT_SOURCE_DIR}/hash_table/static_reduction_map_bench.cu") + ################################################################################################### set(RBK_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/reduce_by_key/reduce_by_key.cu") ConfigureBench(RBK_BENCH "${RBK_BENCH_SRC}") diff --git a/benchmarks/hash_table/static_map_bench.cu b/benchmarks/hash_table/static_map_bench.cu index 165465518..563769df6 100644 --- a/benchmarks/hash_table/static_map_bench.cu +++ b/benchmarks/hash_table/static_map_bench.cu @@ -15,40 +15,38 @@ */ #include -#include "cuco/static_map.cuh" -#include #include -#include +#include +#include #include +#include #include +#include "cuco/static_map.cuh" -enum class dist_type { - UNIQUE, - UNIFORM, - GAUSSIAN -}; +enum class dist_type { UNIQUE, UNIFORM, GAUSSIAN }; -template -static void generate_keys(OutputIt output_begin, OutputIt output_end) { +template +static void generate_keys(OutputIt output_begin, OutputIt output_end) +{ auto num_keys = std::distance(output_begin, output_end); - + std::random_device rd; std::mt19937 gen{rd()}; - switch(Dist) { + switch (Dist) { case dist_type::UNIQUE: - for(auto i = 0; i < num_keys; ++i) { + for (auto i = 0; i < num_keys; ++i) { output_begin[i] = i; } break; case dist_type::UNIFORM: - for(auto i = 0; i < num_keys; ++i) { + for (auto i = 0; i < num_keys; ++i) { output_begin[i] = std::abs(static_cast(gen())); } break; case dist_type::GAUSSIAN: std::normal_distribution<> dg{1e9, 1e7}; - for(auto i = 0; i < num_keys; ++i) { + for (auto i = 0; i < num_keys; ++i) { output_begin[i] = std::abs(static_cast(dg(gen))); } break; @@ -59,88 +57,84 @@ static void generate_keys(OutputIt output_begin, OutputIt output_end) { * @brief Generates input sizes and hash table occupancies * */ -static void generate_size_and_occupancy(benchmark::internal::Benchmark* b) { - for (auto size = 100'000'000; size <= 100'000'000; size *= 10) { - for (auto occupancy = 10; occupancy <= 90; occupancy += 10) { +static void generate_size_and_occupancy(benchmark::internal::Benchmark* b) +{ + for (auto size = 4096; size <= 1 << 28; size *= 2) { + for (auto occupancy = 60; occupancy <= 60; occupancy += 10) { b->Args({size, occupancy}); } } } - - template -static void BM_static_map_insert(::benchmark::State& state) { +static void BM_static_map_insert(::benchmark::State& state) +{ using map_type = cuco::static_map; - + std::size_t num_keys = state.range(0); - float occupancy = state.range(1) / float{100}; - std::size_t size = num_keys / occupancy; + float occupancy = state.range(1) / float{100}; + std::size_t size = num_keys / occupancy; + + std::vector h_keys(num_keys); + std::vector> h_pairs(num_keys); - std::vector h_keys( num_keys ); - std::vector> h_pairs( num_keys ); - generate_keys(h_keys.begin(), h_keys.end()); - - for(auto i = 0; i < num_keys; ++i) { - Key key = h_keys[i]; - Value val = h_keys[i]; - h_pairs[i].first = key; + + for (auto i = 0; i < num_keys; ++i) { + Key key = h_keys[i]; + Value val = h_keys[i]; + h_pairs[i].first = key; h_pairs[i].second = val; } - thrust::device_vector> d_pairs( h_pairs ); + thrust::device_vector> d_pairs(h_pairs); - for(auto _ : state) { - state.ResumeTiming(); - state.PauseTiming(); + for (auto _ : state) { map_type map{size, -1, -1}; - state.ResumeTiming(); - - map.insert(d_pairs.begin(), d_pairs.end()); - state.PauseTiming(); + { + cuda_event_timer raii{state}; + map.insert(d_pairs.begin(), d_pairs.end()); + } } - state.SetBytesProcessed((sizeof(Key) + sizeof(Value)) * - int64_t(state.iterations()) * + state.SetBytesProcessed((sizeof(Key) + sizeof(Value)) * int64_t(state.iterations()) * int64_t(state.range(0))); } - - template -static void BM_static_map_search_all(::benchmark::State& state) { +static void BM_static_map_search_all(::benchmark::State& state) +{ using map_type = cuco::static_map; - + std::size_t num_keys = state.range(0); - float occupancy = state.range(1) / float{100}; - std::size_t size = num_keys / occupancy; + float occupancy = state.range(1) / float{100}; + std::size_t size = num_keys / occupancy; map_type map{size, -1, -1}; auto view = map.get_device_mutable_view(); - std::vector h_keys( num_keys ); - std::vector h_values( num_keys ); - std::vector> h_pairs ( num_keys ); - std::vector h_results (num_keys); + std::vector h_keys(num_keys); + std::vector h_values(num_keys); + std::vector> h_pairs(num_keys); + std::vector h_results(num_keys); generate_keys(h_keys.begin(), h_keys.end()); - - for(auto i = 0; i < num_keys; ++i) { - Key key = h_keys[i]; - Value val = h_keys[i]; - h_pairs[i].first = key; + + for (auto i = 0; i < num_keys; ++i) { + Key key = h_keys[i]; + Value val = h_keys[i]; + h_pairs[i].first = key; h_pairs[i].second = val; } - thrust::device_vector d_keys( h_keys ); - thrust::device_vector d_results( num_keys); - thrust::device_vector> d_pairs( h_pairs ); + thrust::device_vector d_keys(h_keys); + thrust::device_vector d_results(num_keys); + thrust::device_vector> d_pairs(h_pairs); map.insert(d_pairs.begin(), d_pairs.end()); - - for(auto _ : state) { + + for (auto _ : state) { map.find(d_keys.begin(), d_keys.end(), d_results.begin()); } @@ -148,52 +142,62 @@ static void BM_static_map_search_all(::benchmark::State& state) { int64_t(state.range(0))); } - - BENCHMARK_TEMPLATE(BM_static_map_insert, int32_t, int32_t, dist_type::UNIQUE) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); BENCHMARK_TEMPLATE(BM_static_map_search_all, int32_t, int32_t, dist_type::UNIQUE) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); BENCHMARK_TEMPLATE(BM_static_map_insert, int32_t, int32_t, dist_type::UNIFORM) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); BENCHMARK_TEMPLATE(BM_static_map_search_all, int32_t, int32_t, dist_type::UNIFORM) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); BENCHMARK_TEMPLATE(BM_static_map_insert, int32_t, int32_t, dist_type::GAUSSIAN) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); BENCHMARK_TEMPLATE(BM_static_map_search_all, int32_t, int32_t, dist_type::GAUSSIAN) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); BENCHMARK_TEMPLATE(BM_static_map_insert, int64_t, int64_t, dist_type::UNIQUE) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); BENCHMARK_TEMPLATE(BM_static_map_search_all, int64_t, int64_t, dist_type::UNIQUE) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); BENCHMARK_TEMPLATE(BM_static_map_insert, int64_t, int64_t, dist_type::UNIFORM) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); BENCHMARK_TEMPLATE(BM_static_map_search_all, int64_t, int64_t, dist_type::UNIFORM) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); BENCHMARK_TEMPLATE(BM_static_map_insert, int64_t, int64_t, dist_type::GAUSSIAN) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); BENCHMARK_TEMPLATE(BM_static_map_search_all, int64_t, int64_t, dist_type::GAUSSIAN) ->Unit(benchmark::kMillisecond) + ->UseManualTime() ->Apply(generate_size_and_occupancy); \ No newline at end of file diff --git a/benchmarks/hash_table/static_reduction_map_bench.cu b/benchmarks/hash_table/static_reduction_map_bench.cu new file mode 100644 index 000000000..92a2ab788 --- /dev/null +++ b/benchmarks/hash_table/static_reduction_map_bench.cu @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2020, 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 "cuco/static_reduction_map.cuh" + +enum class dist_type { UNIQUE, UNIFORM, GAUSSIAN }; + +template +static void generate_keys(OutputIt output_begin, OutputIt output_end) +{ + auto num_keys = std::distance(output_begin, output_end); + + std::random_device rd; + std::mt19937 gen{rd()}; + + switch (Dist) { + case dist_type::UNIQUE: + for (auto i = 0; i < num_keys; ++i) { + output_begin[i] = i; + } + break; + case dist_type::UNIFORM: + for (auto i = 0; i < num_keys; ++i) { + output_begin[i] = std::abs(static_cast(gen())); + } + break; + case dist_type::GAUSSIAN: + std::normal_distribution<> dg{1e9, 1e7}; + for (auto i = 0; i < num_keys; ++i) { + output_begin[i] = std::abs(static_cast(dg(gen))); + } + break; + } +} + +/** + * @brief Generates input sizes and hash table occupancies + * + */ +static void generate_size_and_occupancy(benchmark::internal::Benchmark* b) +{ + for (auto size = 4096; size <= 1 << 28; size *= 2) { + for (auto occupancy = 60; occupancy <= 60; occupancy += 10) { + b->Args({size, occupancy}); + } + } +} + +template typename ReductionOp> +static void BM_static_map_insert(::benchmark::State& state) +{ + using map_type = cuco::static_reduction_map, Key, Value>; + + std::size_t num_keys = state.range(0); + float occupancy = state.range(1) / float{100}; + std::size_t size = num_keys / occupancy; + + std::vector h_keys(num_keys); + std::vector> h_pairs(num_keys); + + generate_keys(h_keys.begin(), h_keys.end()); + + thrust::device_vector d_keys(h_keys); + thrust::device_vector d_values(h_keys); + + auto pairs_begin = + thrust::make_zip_iterator(thrust::make_tuple(d_keys.begin(), d_values.begin())); + auto pairs_end = pairs_begin + num_keys; + + for (auto _ : state) { + map_type map{size, -1}; + { + cuda_event_timer raii{state}; + map.insert(pairs_begin, pairs_end); + } + } + + state.SetBytesProcessed((sizeof(Key) + sizeof(Value)) * int64_t(state.iterations()) * + int64_t(state.range(0))); +} + +BENCHMARK_TEMPLATE(BM_static_map_insert, int32_t, int32_t, dist_type::UNIQUE, cuco::reduce_add) + ->Unit(benchmark::kMillisecond) + ->UseManualTime() + ->Apply(generate_size_and_occupancy); + +BENCHMARK_TEMPLATE(BM_static_map_insert, int32_t, int32_t, dist_type::UNIFORM, cuco::reduce_add) + ->Unit(benchmark::kMillisecond) + ->UseManualTime() + ->Apply(generate_size_and_occupancy); + +BENCHMARK_TEMPLATE(BM_static_map_insert, int32_t, int32_t, dist_type::GAUSSIAN, cuco::reduce_add) + ->Unit(benchmark::kMillisecond) + ->UseManualTime() + ->Apply(generate_size_and_occupancy); + +BENCHMARK_TEMPLATE(BM_static_map_insert, int64_t, int64_t, dist_type::UNIQUE, cuco::reduce_add) + ->Unit(benchmark::kMillisecond) + ->UseManualTime() + ->Apply(generate_size_and_occupancy); + +BENCHMARK_TEMPLATE(BM_static_map_insert, int64_t, int64_t, dist_type::UNIFORM, cuco::reduce_add) + ->Unit(benchmark::kMillisecond) + ->UseManualTime() + ->Apply(generate_size_and_occupancy); + +BENCHMARK_TEMPLATE(BM_static_map_insert, int64_t, int64_t, dist_type::GAUSSIAN, cuco::reduce_add) + ->Unit(benchmark::kMillisecond) + ->UseManualTime() + ->Apply(generate_size_and_occupancy); \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index a70b53da8..e840e1905 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -15,7 +15,7 @@ endif() function(ConfigureExample EXAMPLE_NAME EXAMPLE_SRC) add_executable(${EXAMPLE_NAME} "${EXAMPLE_SRC}") set_target_properties(${EXAMPLE_NAME} PROPERTIES - CUDA_ARCHITECTURES ${GPU_ARCHS} + CUDA_ARCHITECTURES "${GPU_ARCHS}" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples") target_include_directories(${EXAMPLE_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 32d77b2a8..471a15a7a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,7 +39,7 @@ function(ConfigureTest TEST_NAME TEST_SRC) $) # Link in the CatchMain object file target_link_libraries(${TEST_NAME} Catch2::Catch2 cuco) set_target_properties(${TEST_NAME} PROPERTIES - CUDA_ARCHITECTURES ${GPU_ARCHS} + CUDA_ARCHITECTURES "${GPU_ARCHS}" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") target_compile_options(${TEST_NAME} PRIVATE --expt-extended-lambda --expt-relaxed-constexpr) catch_discover_tests(${TEST_NAME}) From 2a38d70c4fc01d98a324d627b41bff739b8f5a66 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 13 May 2021 11:57:30 -0500 Subject: [PATCH 27/69] Remove redundant ctor. --- include/cuco/detail/pair.cuh | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/include/cuco/detail/pair.cuh b/include/cuco/detail/pair.cuh index de75ad680..dfdf7632e 100644 --- a/include/cuco/detail/pair.cuh +++ b/include/cuco/detail/pair.cuh @@ -68,8 +68,8 @@ struct is_thrust_pair_like_impl : std::false_type { template struct is_thrust_pair_like_impl(std::declval())), - decltype(thrust::get<1>(std::declval()))>> + std::void_t(std::declval())), + decltype(thrust::get<1>(std::declval()))>> : std::conditional_t::value == 2, std::true_type, std::false_type> { }; @@ -116,10 +116,6 @@ struct alignas(detail::pair_alignment()) pair { thrust::get<1>(thrust::raw_reference_cast(t))} { } - __host__ __device__ constexpr pair(First const& f, Second const& s) noexcept - : first{f}, second{s} - { - } }; template From f2d1a2607c9e36b56bbac5baea47dc5066d78327 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 13 May 2021 11:57:59 -0500 Subject: [PATCH 28/69] Add initial static_reduction_map example. --- examples/CMakeLists.txt | 2 + examples/static_reduction_map.cu | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 examples/static_reduction_map.cu diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e840e1905..be1a760e6 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -28,3 +28,5 @@ endfunction(ConfigureExample) ################################################################################################### ConfigureExample(STATIC_MAP_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/static_map/static_map_example.cu") + +ConfigureExample(STATIC_REDUCTION_MAP_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/static_reduction_map.cu") diff --git a/examples/static_reduction_map.cu b/examples/static_reduction_map.cu new file mode 100644 index 000000000..c3921ad10 --- /dev/null +++ b/examples/static_reduction_map.cu @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020, 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 + +/** + * @file host_bulk_example.cu + * @brief Demonstrates usage of the static_map "bulk" host APIs. + * + * The bulk APIs are only invocable from the host and are used for doing operations like insert or + * find on a set of keys. + * + */ + +int main(void) +{ + using Key = int; + using Value = int; + + // Empty slots are represented by reserved "sentinel" values. These values should be selected such + // that they never occur in your input data. + Key const empty_key_sentinel = -1; + + // Number of key/value pairs to be inserted + std::size_t num_keys = 50'000; + + // Compute capacity based on a 50% load factor + auto const load_factor = 0.5; + std::size_t const capacity = std::ceil(num_keys / load_factor); + + // Constructs a map each key with "capacity" slots using -1 as the + // empty key sentinel. The initial payload value for empty slots is determined by the identity of + // the reduction operation. By using the `reduce_add` operation, all values associated with a + // given key will be summed. + cuco::static_reduction_map, Key, Value> map{capacity, empty_key_sentinel}; + + // Create a sequence of random keys in `[0, num_keys/2]` + thrust::device_vector insert_keys(num_keys); + thrust::transform(thrust::device, + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(insert_keys.size()), + insert_keys.begin(), + [=] __device__(auto i) { + thrust::default_random_engine rng(i); + thrust::uniform_int_distribution dist{std::size_t{0}, num_keys/2}; + return dist(rng); + }); + + // Insert each key with a payload of `1` to count the number of times each key was inserted by + // using the `reduce_add` op + auto zipped = thrust::make_zip_iterator( + thrust::make_tuple(insert_keys.begin(), thrust::make_constant_iterator(1))); + + // Inserts all pairs into the map, accumulating the payloads with the `reduce_add` operation + map.insert(zipped, zipped + insert_keys.size()); + + std::cout << "Num unique keys: " << map.get_size() << std::endl; + +} \ No newline at end of file From 3c797013e3504a3ce9396430f553be91d7a3bec0 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 13 May 2021 11:58:15 -0500 Subject: [PATCH 29/69] Remove cuda_memcmp header. --- include/cuco/static_reduction_map.cuh | 1 - 1 file changed, 1 deletion(-) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index b13c1f5af..ebddaebc7 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -24,7 +24,6 @@ #include #include -#include #ifndef CUDART_VERSION #error CUDART_VERSION Undefined! #elif (CUDART_VERSION >= 11000) // including with CUDA 10.2 leads to compilation errors From 8261d939e6f962b10093b37fb58410c8a9d7223e Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Wed, 19 May 2021 12:06:54 -0500 Subject: [PATCH 30/69] Add unsafe accessors to raw slots via reinterpret_cast. --- include/cuco/static_reduction_map.cuh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index ebddaebc7..3f98737c8 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -972,6 +972,19 @@ class static_reduction_map { } private: + /// Unsafe access to the slots stripping away their atomic-ness to allow non-atomic access. This + /// is a temporary solution until we have atomic_ref + value_type* raw_slots_begin() noexcept { return reinterpret_cast(slots_); } + + value_type const* raw_slots_begin() const noexcept + { + return reinterpret_cast(slots_); + } + + value_type* raw_slots_end() noexcept { return raw_slots_begin() + get_capacity(); } + + value_type const* raw_slots_end() const noexcept { return raw_slots_begin() + get_capacity(); } + pair_atomic_type* slots_{nullptr}; ///< Pointer to flat slots storage std::size_t capacity_{}; ///< Total number of slots std::size_t size_{}; ///< Number of keys in map From c6daa09029a9305203c7438376039a817185cad9 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Wed, 19 May 2021 12:07:20 -0500 Subject: [PATCH 31/69] Add retreive_all implementation. --- include/cuco/detail/static_reduction_map.inl | 39 ++++++++++++++++++++ include/cuco/static_reduction_map.cuh | 23 ++++++++++++ 2 files changed, 62 insertions(+) diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index 86b500e4e..bd9907ebc 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -112,6 +112,45 @@ void static_reduction_map::find( CUCO_CUDA_TRY(cudaDeviceSynchronize()); } +namespace detail { +template +struct slot_to_tuple { + template + __device__ thrust::tuple operator()(S const& s) + { + return thrust::tuple(s.first, s.second); + } +}; + +template +struct slot_is_filled { + Key empty_key_sentinel; + template + __device__ bool operator()(S const& s) + { + return thrust::get<0>(s) != empty_key_sentinel; + } +}; +} // namespace detail + +template +template +void static_reduction_map::retrieve_all( + KeyOut keys_out, ValueOut values_out) +{ + // Convert pair_type to thrust::tuple to allow assigning to a zip iterator + auto begin = thrust::make_transform_iterator(raw_slots_begin(), detail::slot_to_tuple{}); + auto end = begin + get_capacity(); + auto filled = detail::slot_is_filled{get_empty_key_sentinel()}; + auto zipped_out = thrust::make_zip_iterator(thrust::make_tuple(keys_out, values_out)); + + thrust::copy_if(thrust::device, begin, end, zipped_out, filled); +} + template #include #include +#include +#include +#include +#include #include #ifndef CUDART_VERSION @@ -276,6 +280,25 @@ class static_reduction_map { Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}) noexcept; + /** + * @brief Retrieves all of the keys and their associated values. + * + * The order in which keys are returned is implementation defined and not guaranteed to be + * consistent between subsequent calls to `retrieve_all`. + * + * Behavior is undefined if the range beginning at `keys_out` or `values_out` is not large enough + * to contain the number of keys in the map. + * + * @tparam KeyOut Device accessible random access output iterator whose `value_type` is + * convertible from `key_type`. + * @tparam ValueOut Device accesible random access output iterator whose `value_type` is + * convertible from `mapped_type`. + * @param keys_out Beginning output iterator for keys + * @param values_out Beginning output iterator for values + */ + template + void retrieve_all(KeyOut keys_out, ValueOut values_out); + /** * @brief Indicates whether the keys in the range `[first, last)` are contained in the map. * From 62a99ab6fb9f28614624879168eba468dcc648de Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Wed, 19 May 2021 12:07:33 -0500 Subject: [PATCH 32/69] Add retrieve_all to example. --- examples/static_reduction_map.cu | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/static_reduction_map.cu b/examples/static_reduction_map.cu index c3921ad10..f152ceb78 100644 --- a/examples/static_reduction_map.cu +++ b/examples/static_reduction_map.cu @@ -45,7 +45,7 @@ int main(void) Key const empty_key_sentinel = -1; // Number of key/value pairs to be inserted - std::size_t num_keys = 50'000; + std::size_t num_keys = 257; // Compute capacity based on a 50% load factor auto const load_factor = 0.5; @@ -64,8 +64,9 @@ int main(void) thrust::make_counting_iterator(insert_keys.size()), insert_keys.begin(), [=] __device__(auto i) { - thrust::default_random_engine rng(i); - thrust::uniform_int_distribution dist{std::size_t{0}, num_keys/2}; + thrust::default_random_engine rng; + thrust::uniform_int_distribution dist{0, 10}; + rng.discard(i); return dist(rng); }); @@ -79,4 +80,12 @@ int main(void) std::cout << "Num unique keys: " << map.get_size() << std::endl; + thrust::device_vector unique_keys(map.get_size()); + thrust::device_vector count_per_key(map.get_size()); + + map.retrieve_all(unique_keys.begin(), count_per_key.begin()); + + for (int i = 0; i < unique_keys.size(); ++i) { + std::cout << "Key: " << unique_keys[i] << " Count: " << count_per_key[i] << std::endl; + } } \ No newline at end of file From c1fe449e1aaf68c15393e777774032a0740859de Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Sun, 1 Aug 2021 21:07:17 +0000 Subject: [PATCH 33/69] Sync static_reduction_map with latest changes in static_map. --- examples/static_reduction_map.cu | 18 +- include/cuco/detail/static_reduction_map.inl | 67 +++--- .../detail/static_reduction_map_kernels.cuh | 43 ++-- include/cuco/detail/traits.hpp | 54 +++++ include/cuco/static_reduction_map.cuh | 195 ++++++++++++------ 5 files changed, 256 insertions(+), 121 deletions(-) create mode 100644 include/cuco/detail/traits.hpp diff --git a/examples/static_reduction_map.cu b/examples/static_reduction_map.cu index f152ceb78..8d3839658 100644 --- a/examples/static_reduction_map.cu +++ b/examples/static_reduction_map.cu @@ -27,14 +27,12 @@ #include /** - * @file host_bulk_example.cu - * @brief Demonstrates usage of the static_map "bulk" host APIs. + * @brief Demonstrates usage of the static_reduction_map "bulk" host APIs. * * The bulk APIs are only invocable from the host and are used for doing operations like insert or * find on a set of keys. * */ - int main(void) { using Key = int; @@ -45,11 +43,14 @@ int main(void) Key const empty_key_sentinel = -1; // Number of key/value pairs to be inserted - std::size_t num_keys = 257; + std::size_t const num_elems = 256; + + // average number of values per distinct key + std::size_t const multiplicity = 4; // Compute capacity based on a 50% load factor auto const load_factor = 0.5; - std::size_t const capacity = std::ceil(num_keys / load_factor); + std::size_t const capacity = std::ceil(num_elems / load_factor); // Constructs a map each key with "capacity" slots using -1 as the // empty key sentinel. The initial payload value for empty slots is determined by the identity of @@ -57,15 +58,16 @@ int main(void) // given key will be summed. cuco::static_reduction_map, Key, Value> map{capacity, empty_key_sentinel}; - // Create a sequence of random keys in `[0, num_keys/2]` - thrust::device_vector insert_keys(num_keys); + // Create a sequence of random keys + thrust::device_vector insert_keys(num_elems); thrust::transform(thrust::device, thrust::make_counting_iterator(0), thrust::make_counting_iterator(insert_keys.size()), insert_keys.begin(), [=] __device__(auto i) { thrust::default_random_engine rng; - thrust::uniform_int_distribution dist{0, 10}; + thrust::uniform_int_distribution dist( + Key{1}, static_cast(num_elems / multiplicity)); rng.discard(i); return dist(rng); }); diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index bd9907ebc..dcb385ae0 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -14,6 +14,8 @@ * limitations under the License. */ +#include + namespace cuco { /**---------------------------------------------------------------------------* @@ -23,7 +25,7 @@ namespace cuco { enum class insert_result { CONTINUE, ///< Insert did not succeed, continue trying to insert SUCCESS, ///< New pair inserted successfully - DUPLICATE ///< Insert did not succeed, key is already present + DUPLICATE ///< Key is already present }; template ::static_reductio op_{reduction_op}, slot_allocator_{alloc} { - slots_ = std::allocator_traits::allocate(slot_allocator_, capacity); + slots_ = std::allocator_traits::allocate(slot_allocator_, capacity_); auto constexpr block_size = 256; auto constexpr stride = 4; - auto const grid_size = (capacity + stride * block_size - 1) / (stride * block_size); - detail::initialize<<>>( + auto const grid_size = (capacity_ + stride * block_size - 1) / (stride * block_size); + detail::initialize<<>>( slots_, get_empty_key_sentinel(), get_empty_value_sentinel(), get_capacity()); CUCO_CUDA_TRY(cudaMallocManaged(&num_successes_, sizeof(atomic_ctr_type))); @@ -58,7 +60,7 @@ template ::~static_reduction_map() { std::allocator_traits::deallocate(slot_allocator_, slots_, capacity_); - CUCO_CUDA_TRY(cudaFree(num_successes_)); + CUCO_ASSERT_CUDA_SUCCESS(cudaFree(num_successes_)); } template ::insert(Inp Hash hash, KeyEqual key_equal) { - auto num_keys = std::distance(first, last); + auto num_keys = std::distance(first, last); + if (num_keys == 0) { return; } + auto const block_size = 128; auto const stride = 1; auto const tile_size = 4; @@ -98,9 +102,11 @@ template template void static_reduction_map::find( - InputIt first, InputIt last, OutputIt output_begin, Hash hash, KeyEqual key_equal) noexcept + InputIt first, InputIt last, OutputIt output_begin, Hash hash, KeyEqual key_equal) { - auto num_keys = std::distance(first, last); + auto num_keys = std::distance(first, last); + if (num_keys == 0) { return; } + auto const block_size = 128; auto const stride = 1; auto const tile_size = 4; @@ -143,7 +149,8 @@ void static_reduction_map::retrieve_a KeyOut keys_out, ValueOut values_out) { // Convert pair_type to thrust::tuple to allow assigning to a zip iterator - auto begin = thrust::make_transform_iterator(raw_slots_begin(), detail::slot_to_tuple{}); + auto begin = + thrust::make_transform_iterator(raw_slots_begin(), detail::slot_to_tuple{}); auto end = begin + get_capacity(); auto filled = detail::slot_is_filled{get_empty_key_sentinel()}; auto zipped_out = thrust::make_zip_iterator(thrust::make_tuple(keys_out, values_out)); @@ -158,9 +165,11 @@ template template void static_reduction_map::contains( - InputIt first, InputIt last, OutputIt output_begin, Hash hash, KeyEqual key_equal) noexcept + InputIt first, InputIt last, OutputIt output_begin, Hash hash, KeyEqual key_equal) { - auto num_keys = std::distance(first, last); + auto num_keys = std::distance(first, last); + if (num_keys == 0) { return; } + auto const block_size = 128; auto const stride = 1; auto const tile_size = 4; @@ -178,7 +187,7 @@ template template -__device__ Value +__device__ bool static_reduction_map::device_mutable_view::insert( value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept { @@ -195,7 +204,10 @@ static_reduction_map::device_mutable_ slot_key.compare_exchange_strong(expected_key, insert_pair.first, memory_order_relaxed); if (key_success or key_equal(insert_pair.first, expected_key)) { - return this->get_op().apply(slot_value, insert_pair.second); + this->get_op().apply(slot_value, insert_pair.second); + + // only return true if a new has been inserted + return key_success; } // if we couldn't insert the key, but it wasn't a duplicate, then there must @@ -212,7 +224,7 @@ template __device__ bool static_reduction_map::device_mutable_view::insert( - CG g, value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept + CG const& g, value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(g, insert_pair.first, hash); @@ -224,7 +236,7 @@ static_reduction_map::device_mutable_ // The user provided `key_equal` should never be used to compare against `empty_key_sentinel` as // the sentinel is not a valid key value. Therefore, first check for the sentinel // TODO: Use memcmp - auto const slot_is_empty = (current_key == this->get_empty_key_sentinel()); + auto const slot_is_empty = detail::bitwise_compare(current_key, this->get_empty_key_sentinel()); auto const key_exists = not slot_is_empty and key_equal(current_key, insert_pair.first); @@ -287,7 +299,9 @@ __device__ while (true) { auto const existing_key = current_slot->first.load(cuda::std::memory_order_relaxed); // Key doesn't exist, return end() - if (existing_key == this->get_empty_key_sentinel()) { return this->end(); } + if (detail::bitwise_compare(existing_key, this->get_empty_key_sentinel())) { + return this->end(); + } // Key exists, return iterator to location if (key_equal(existing_key, k)) { return current_slot; } @@ -312,7 +326,9 @@ __device__ typename static_reduction_mapfirst.load(cuda::std::memory_order_relaxed); // Key doesn't exist, return end() - if (existing_key == this->get_empty_key_sentinel()) { return this->end(); } + if (detail::bitwise_compare(existing_key, this->get_empty_key_sentinel())) { + return this->end(); + } // Key exists, return iterator to location if (key_equal(existing_key, k)) { return current_slot; } @@ -330,7 +346,7 @@ template __device__ typename static_reduction_map::device_view::iterator static_reduction_map::device_view::find( - CG g, Key const& k, Hash hash, KeyEqual key_equal) noexcept + CG const& g, Key const& k, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(g, k, hash); @@ -339,7 +355,8 @@ __device__ // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as // the sentinel is not a valid key value. Therefore, first check for the sentinel - auto const slot_is_empty = (existing_key == this->get_empty_key_sentinel()); + auto const slot_is_empty = + detail::bitwise_compare(existing_key, this->get_empty_key_sentinel()); // the key we were searching for was found by one of the threads, // so we return an iterator to the entry @@ -370,7 +387,7 @@ template __device__ typename static_reduction_map::device_view:: const_iterator static_reduction_map::device_view::find( - CG g, Key const& k, Hash hash, KeyEqual key_equal) const noexcept + CG const& g, Key const& k, Hash hash, KeyEqual key_equal) const noexcept { auto current_slot = initial_slot(g, k, hash); @@ -379,7 +396,8 @@ __device__ typename static_reduction_mapget_empty_key_sentinel()); + auto const slot_is_empty = + detail::bitwise_compare(existing_key, this->get_empty_key_sentinel()); // the key we were searching for was found by one of the threads, so we return an iterator to // the entry @@ -418,7 +436,7 @@ static_reduction_map::device_view::co while (true) { auto const existing_key = current_slot->first.load(cuda::std::memory_order_relaxed); - if (existing_key == empty_key_sentinel_) { return false; } + if (detail::bitwise_compare(existing_key, empty_key_sentinel_)) { return false; } if (key_equal(existing_key, k)) { return true; } @@ -434,7 +452,7 @@ template __device__ bool static_reduction_map::device_view::contains( - CG g, Key const& k, Hash hash, KeyEqual key_equal) noexcept + CG const& g, Key const& k, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(g, k, hash); @@ -443,7 +461,8 @@ static_reduction_map::device_view::co // The user provide `key_equal` can never be used to compare against `empty_key_sentinel` as // the sentinel is not a valid key value. Therefore, first check for the sentinel - auto const slot_is_empty = (existing_key == this->get_empty_key_sentinel()); + auto const slot_is_empty = + detail::bitwise_compare(existing_key, this->get_empty_key_sentinel()); // the key we were searching for was found by one of the threads, so we return an iterator to // the entry diff --git a/include/cuco/detail/static_reduction_map_kernels.cuh b/include/cuco/detail/static_reduction_map_kernels.cuh index 9849efb44..93c86d5ff 100644 --- a/include/cuco/detail/static_reduction_map_kernels.cuh +++ b/include/cuco/detail/static_reduction_map_kernels.cuh @@ -34,18 +34,19 @@ namespace cg = cooperative_groups; * @param v Value to which all values in `slots` are initialized * @param size Size of the storage pointed to by `slots` */ -template __global__ void initialize(pair_atomic_type* const slots, Key k, Value v, std::size_t size) { - auto tid = threadIdx.x + blockIdx.x * blockDim.x; + auto tid = block_size * blockIdx.x + threadIdx.x; while (tid < size) { new (&slots[tid].first) atomic_key_type{k}; new (&slots[tid].second) atomic_mapped_type{v}; - tid += gridDim.x * blockDim.x; + tid += gridDim.x * block_size; } } @@ -69,7 +70,7 @@ __global__ void initialize(pair_atomic_type* const slots, Key k, Value v, std::s * @param hash The unary function to apply to hash each key * @param key_equal The binary function used to compare two keys for equality */ -template (cg::this_thread_block()); - auto tid = blockDim.x * blockIdx.x + threadIdx.x; + auto tid = block_size * blockIdx.x + threadIdx.x; auto it = first + tid / tile_size; while (it < last) { @@ -149,7 +150,7 @@ __global__ void insert( if (view.insert(tile, insert_pair, hash, key_equal) && tile.thread_rank() == 0) { thread_num_successes++; } - it += (gridDim.x * blockDim.x) / tile_size; + it += (gridDim.x * block_size) / tile_size; } // compute number of successfully inserted elements for each block @@ -179,7 +180,7 @@ __global__ void insert( * @param hash The unary function to apply to hash each key * @param key_equal The binary function to compare two keys for equality */ -template second.load(cuda::std::memory_order_relaxed); __syncthreads(); *(output_begin + key_idx) = writeBuffer[threadIdx.x]; - key_idx += gridDim.x * blockDim.x; + key_idx += gridDim.x * block_size; } } @@ -237,7 +238,7 @@ __global__ void find( * @param hash The unary function to apply to hash each key * @param key_equal The binary function to compare two keys for equality */ -template (cg::this_thread_block()); - auto tid = blockDim.x * blockIdx.x + threadIdx.x; + auto tid = block_size * blockIdx.x + threadIdx.x; auto key_idx = tid / tile_size; __shared__ Value writeBuffer[block_size]; @@ -271,7 +272,7 @@ __global__ void find( if (tile.thread_rank() == 0) { *(output_begin + key_idx) = writeBuffer[threadIdx.x / tile_size]; } - key_idx += (gridDim.x * blockDim.x) / tile_size; + key_idx += (gridDim.x * block_size) / tile_size; } } @@ -295,7 +296,7 @@ __global__ void find( * @param hash The unary function to apply to hash each key * @param key_equal The binary function to compare two keys for equality */ -template (cg::this_thread_block()); - auto tid = blockDim.x * blockIdx.x + threadIdx.x; + auto tid = block_size * blockIdx.x + threadIdx.x; auto key_idx = tid / tile_size; __shared__ bool writeBuffer[block_size]; @@ -381,7 +382,7 @@ __global__ void contains( if (tile.thread_rank() == 0) { *(output_begin + key_idx) = writeBuffer[threadIdx.x / tile_size]; } - key_idx += (gridDim.x * blockDim.x) / tile_size; + key_idx += (gridDim.x * block_size) / tile_size; } } diff --git a/include/cuco/detail/traits.hpp b/include/cuco/detail/traits.hpp new file mode 100644 index 000000000..53ef38433 --- /dev/null +++ b/include/cuco/detail/traits.hpp @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021, 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 + */ + +#pragma once + +namespace cuco { +/** + * @brief Customization point that can be specialized to indicate that it is safe to perform bitwise + * equality comparisons on objects of type `T`. + * + * By default, only types where `std::has_unique_object_representations_v` is true are safe for + * bitwise equality. However, this can be too restrictive for some types, e.g., floating point + * types. + * + * User-defined specializations of `is_bitwise_comparable` are allowed, but it is the users + * responsibility to ensure values do not occur that would lead to unexpected behavior. For example, + * if a `NaN` bit pattern were used as the empty sentinel value, it may not compare bitwise equal to + * other `NaN` bit patterns. + * + */ +template +struct is_bitwise_comparable : std::false_type { +}; + +/// By default, only types with unique object representations are allowed +template +struct is_bitwise_comparable>> + : std::true_type { +}; + +/** + * @brief Declares that a type `Type` is bitwise comparable. + * + */ +#define CUCO_DECLARE_BITWISE_COMPARABLE(Type) \ + namespace cuco { \ + template <> \ + struct is_bitwise_comparable : std::true_type { \ + }; \ + } + +} // namespace cuco \ No newline at end of file diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index c66958fb8..56c62bde8 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -17,20 +17,24 @@ #pragma once #include +#include #include #include -#include -#include -#include -#include #include #include #include +#include +#include +#include #include -#ifndef CUDART_VERSION -#error CUDART_VERSION Undefined! -#elif (CUDART_VERSION >= 11000) // including with CUDA 10.2 leads to compilation errors + +#if defined(CUDART_VERSION) && (CUDART_VERSION >= 11000) && defined(__CUDA_ARCH__) && \ + (__CUDA_ARCH__ >= 700) +#define CUCO_HAS_CUDA_BARRIER +#endif + +#if defined(CUCO_HAS_CUDA_BARRIER) #include #endif @@ -38,6 +42,7 @@ #include #include #include +#include namespace cuco { @@ -107,17 +112,18 @@ struct custom_op { /** * @brief A GPU-accelerated, unordered, associative container of key-value - * pairs with unique keys. + * pairs that reduces the values associated to the same key according to a + * functor. * * Allows constant time concurrent inserts or concurrent find operations (not * concurrent insert and find) from threads in device code. * * Current limitations: - * - Requires keys that are Arithmetic + * - Requires key types where `cuco::is_bitwise_comparable::value` is true * - Does not support erasing keys * - Capacity is fixed and will not grow automatically - * - Requires the user to specify sentinel values for both key and mapped value - * to indicate empty slots + * - Requires the user to specify sentinel value for the key to indicate empty + * slots * - Does not support concurrent insert and find operations * * The `static_reduction_map` supports two types of operations: @@ -129,7 +135,7 @@ struct custom_op { * in the map. For example, given a range of keys specified by device-accessible * iterators, the bulk `insert` function will insert all keys into the map. * - * The singular device-side operations allow individual threads to to perform + * The singular device-side operations allow individual threads to perform * independent insert or find/contains operations from device code. These * operations are accessed through non-owning, trivially copyable "view" types: * `device_view` and `mutable_device_view`. The `device_view` class is an @@ -138,26 +144,51 @@ struct custom_op { * The two types are separate to prevent erroneous concurrent insert/find * operations. * - * Example: - * \code{.cpp} - * int empty_key_sentinel = -1; - * int empty_value_sentine = -1; + * Example: + * \code{.cpp} + * + * // Empty slots are represented by reserved "sentinel" values. These values should be selected + * such + * // that they never occur in your input data. + * int const empty_key_sentinel = -1; + * + * // Number of key/value pairs to be inserted + * std::size_t const num_elems = 256; + * + * // average number of values per distinct key + * std::size_t const multiplicity = 4; + * + * // Compute capacity based on a 50% load factor + * auto const load_factor = 0.5; + * std::size_t const capacity = std::ceil(num_elems / load_factor); * - * // Constructs a map with 100,000 slots using -1 and -1 as the empty key/value - * // sentinels. Note the capacity is chosen knowing we will insert 50,000 keys, - * // for an load factor of 50%. - * static_reduction_map m{100'000, empty_key_sentinel, empty_value_sentinel}; + * // Constructs a map each key with "capacity" slots using -1 as the + * // empty key sentinel. The initial payload value for empty slots is determined by the identity of + * // the reduction operation. By using the `reduce_add` operation, all values associated with a + * // given key will be summed. + * cuco::static_reduction_map, int, int> map{capacity, empty_key_sentinel}; * - * // Create a sequence of pairs {{0,0}, {1,1}, ... {i,i}} - * thrust::device_vector> pairs(50,000); - * thrust::transform(thrust::make_counting_iterator(0), - * thrust::make_counting_iterator(pairs.size()), - * pairs.begin(), - * []__device__(auto i){ return thrust::make_pair(i,i); }; + * // Create a sequence of random keys + * thrust::device_vector insert_keys(num_elems); + * thrust::transform(thrust::device, + * thrust::make_counting_iterator(0), + * thrust::make_counting_iterator(insert_keys.size()), + * insert_keys.begin(), + * [=] __device__(auto i) { + * thrust::default_random_engine rng; + * thrust::uniform_int_distribution dist( + * int{1}, static_cast(num_elems / multiplicity)); + * rng.discard(i); + * return dist(rng); + * }); * + * // Insert each key with a payload of `1` to count the number of times each key was inserted by + * // using the `reduce_add` op + * auto zipped = thrust::make_zip_iterator( + * thrust::make_tuple(insert_keys.begin(), thrust::make_constant_iterator(1))); * - * // Inserts all pairs into the map - * m.insert(pairs.begin(), pairs.end()); + * // Inserts all pairs into the map, accumulating the payloads with the `reduce_add` operation + * map.insert(zipped, zipped + insert_keys.size()); * * // Get a `device_view` and passes it to a kernel where threads may perform * // `find/contains` lookups @@ -177,7 +208,11 @@ template > class static_reduction_map { - static_assert(std::is_arithmetic::value, "Unsupported, non-arithmetic key type."); + static_assert( + is_bitwise_comparable::value, + "Key type must have unique object representations or have been explicitly declared as safe for " + "bitwise comparison via specialization of cuco::is_bitwise_comparable."); + static_assert(std::is_same::value, "Type mismatch between ReductionOp::value_type and Value"); @@ -193,32 +228,38 @@ class static_reduction_map { using slot_allocator_type = typename std::allocator_traits::rebind_alloc; +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 700) + static_assert(atomic_key_type::is_always_lock_free, + "A key type larger than 8B is supported for only sm_70 and up."); + static_assert(atomic_mapped_type::is_always_lock_free, + "A value type larger than 8B is supported for only sm_70 and up."); +#endif + static_reduction_map(static_reduction_map const&) = delete; static_reduction_map(static_reduction_map&&) = delete; static_reduction_map& operator=(static_reduction_map const&) = delete; static_reduction_map& operator=(static_reduction_map&&) = delete; /** - * @brief Construct a fixed-size map with the specified capacity and sentinel values. + * @brief Construct a fixed-size map with the specified capacity and sentinel key. * @brief Construct a statically sized map with the specified number of slots - * and sentinel values. + * and sentinel key. * * The capacity of the map is fixed. Insert operations will not automatically * grow the map. Attempting to insert more unique keys than the capacity of - * the map results in undefined behavior. + * the map results in undefined behavior (there should be at least one empty slot). * * Performance begins to degrade significantly beyond a load factor of ~70%. * For best performance, choose a capacity that will keep the load factor * below 70%. E.g., if inserting `N` unique keys, choose a capacity of * `N * (1/0.7)`. * - * The `empty_key_sentinel` and `empty_value_sentinel` values are reserved and - * undefined behavior results from attempting to insert any key/value pair - * that contains either. + * The `empty_key_sentinel` is reserved and undefined behaviour results from + * attempting to insert said key. * * @param capacity The total number of slots in the map * @param empty_key_sentinel The reserved key value for empty slots - * @param empty_value_sentinel The reserved mapped value for empty slots + * @param reduction_op Reduction operator * @param alloc Allocator used for allocating device storage */ static_reduction_map(std::size_t capacity, @@ -235,9 +276,6 @@ class static_reduction_map { /** * @brief Inserts all key/value pairs in the range `[first, last)`. * - * If multiple keys in `[first, last)` compare equal, it is unspecified which - * element is inserted. - * * @tparam InputIt Device accessible input iterator whose `value_type` is * convertible to the map's `value_type` * @tparam Hash Unary callable type @@ -278,7 +316,7 @@ class static_reduction_map { InputIt last, OutputIt output_begin, Hash hash = Hash{}, - KeyEqual key_equal = KeyEqual{}) noexcept; + KeyEqual key_equal = KeyEqual{}); /** * @brief Retrieves all of the keys and their associated values. @@ -324,7 +362,7 @@ class static_reduction_map { InputIt last, OutputIt output_begin, Hash hash = Hash{}, - KeyEqual key_equal = KeyEqual{}) noexcept; + KeyEqual key_equal = KeyEqual{}); private: class device_view_base { @@ -360,7 +398,7 @@ class static_reduction_map { * @brief Gets the binary op * */ - __device__ ReductionOp get_op() const { return op_; } + __device__ ReductionOp get_op() const noexcept { return op_; } /** * @brief Gets slots array. @@ -417,7 +455,7 @@ class static_reduction_map { * @return Pointer to the initial slot for `k` */ template - __device__ iterator initial_slot(CG g, Key const& k, Hash hash) noexcept + __device__ iterator initial_slot(CG const& g, Key const& k, Hash hash) noexcept { return &slots_[(hash(k) + g.thread_rank()) % capacity_]; } @@ -435,7 +473,7 @@ class static_reduction_map { * @return Pointer to the initial slot for `k` */ template - __device__ const_iterator initial_slot(CG g, Key const& k, Hash hash) const noexcept + __device__ const_iterator initial_slot(CG const& g, Key const& k, Hash hash) const noexcept { return &slots_[(hash(k) + g.thread_rank()) % capacity_]; } @@ -475,7 +513,7 @@ class static_reduction_map { * @return The next slot after `s` */ template - __device__ iterator next_slot(CG g, iterator s) noexcept + __device__ iterator next_slot(CG const& g, iterator s) noexcept { uint32_t index = s - slots_; return &slots_[(index + g.size()) % capacity_]; @@ -493,7 +531,7 @@ class static_reduction_map { * @return The next slot after `s` */ template - __device__ const_iterator next_slot(CG g, const_iterator s) const noexcept + __device__ const_iterator next_slot(CG const& g, const_iterator s) const noexcept { uint32_t index = s - slots_; return &slots_[(index + g.size()) % capacity_]; @@ -599,7 +637,7 @@ class static_reduction_map { * * Example: * \code{.cpp} - * cuco::static_reduction_map m{100'000, -1, -1}; + * cuco::static_reduction_mapint,int> m{100'000, -1}; * * // Inserts a sequence of pairs {{0,0}, {1,1}, ... {i,i}} * thrust::for_each(thrust::make_counting_iterator(0), @@ -635,7 +673,17 @@ class static_reduction_map { : device_view_base{slots, capacity, empty_key_sentinel, reduction_op} { } - + template + __device__ static device_mutable_view make_from_uninitialized_slots( + CG const& g, + pair_atomic_type* slots, + std::size_t capacity, + Key empty_key_sentinel, + ReductionOp reduction_op) noexcept + { + device_view_base::initialize_slots(g, slots, capacity, empty_key_sentinel, reduction_op); + return device_mutable_view{slots, capacity, empty_key_sentinel, reduction_op}; + } /** * @brief Inserts the specified key/value pair into the map. * @@ -649,13 +697,13 @@ class static_reduction_map { * @param hash The unary callable used to hash the key * @param key_equal The binary callable used to compare two keys for * equality - * @return `true` if the insert was successful, `false` otherwise. + * @return `true` if the insert (of a new key) was successful, `false` otherwise. */ template , typename KeyEqual = thrust::equal_to> - __device__ Value insert(value_type const& insert_pair, - Hash hash = Hash{}, - KeyEqual key_equal = KeyEqual{}) noexcept; + __device__ bool insert(value_type const& insert_pair, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}) noexcept; /** * @brief Inserts the specified key/value pair into the map. * @@ -666,7 +714,7 @@ class static_reduction_map { * significant boost in throughput compared to the non Cooperative Group * `insert` at moderate to high load factors. * - * @tparam Cooperative Group type + * @tparam CG Cooperative Group type * @tparam Hash Unary callable type * @tparam KeyEqual Binary callable type * @@ -675,16 +723,15 @@ class static_reduction_map { * @param hash The unary callable used to hash the key * @param key_equal The binary callable used to compare two keys for * equality - * @return `true` if the insert was successful, `false` otherwise. + * @return `true` if the insert (of a new key) was successful, `false` otherwise. */ template , typename KeyEqual = thrust::equal_to> - __device__ bool insert(CG g, + __device__ bool insert(CG const& g, value_type const& insert_pair, Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}) noexcept; - }; // class device mutable view /** @@ -710,8 +757,7 @@ class static_reduction_map { * @param capacity The number of slots viewed by this object * @param empty_key_sentinel The reserved value for keys to represent empty * slots - * @param empty_value_sentinel The reserved value for mapped values to - * represent empty slots + * @param reduction_op The reduction functor */ __host__ __device__ device_view(pair_atomic_type* slots, std::size_t capacity, @@ -721,6 +767,19 @@ class static_reduction_map { { } + /** + * @brief Construct a `device_view` from a `device_mutable_view` object + * + * @param mutable_map object of type `device_mutable_view` + */ + __host__ __device__ explicit device_view(device_mutable_view mutable_map) + : device_view_base{mutable_map.get_slots(), + mutable_map.get_capacity(), + mutable_map.get_empty_key_sentinel(), + mutable_map.get_op()} + { + } + /** * @brief Makes a copy of given `device_view` using non-owned memory. * @@ -752,20 +811,18 @@ class static_reduction_map { * @endcode * * @tparam CG The type of the cooperative thread group - * @param g The ooperative thread group used to copy the slots + * @param g The cooperative thread group used to copy the slots * @param source_device_view `device_view` to copy from * @param memory_to_use Array large enough to support `capacity` elements. Object does not take * the ownership of the memory * @return Copy of passed `device_view` */ template - __device__ static device_view make_copy(CG g, + __device__ static device_view make_copy(CG const& g, pair_atomic_type* const memory_to_use, device_view source_device_view) noexcept { -#ifndef CUDART_VERSION -#error CUDART_VERSION Undefined! -#elif (CUDART_VERSION >= 11000) +#if defined(CUDA_HAS_CUDA_BARRIER) __shared__ cuda::barrier barrier; if (g.thread_rank() == 0) { init(&barrier, g.size()); } g.sync(); @@ -791,7 +848,7 @@ class static_reduction_map { return device_view(memory_to_use, source_device_view.get_capacity(), source_device_view.get_empty_key_sentinel(), - source_device_view.get_empty_value_sentinel()); + source_device_view.get_op()); } /** @@ -859,7 +916,7 @@ class static_reduction_map { typename Hash = cuco::detail::MurmurHash3_32, typename KeyEqual = thrust::equal_to> __device__ iterator - find(CG g, Key const& k, Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}) noexcept; + find(CG const& g, Key const& k, Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}) noexcept; /** * @brief Finds the value corresponding to the key `k`. @@ -884,8 +941,10 @@ class static_reduction_map { template , typename KeyEqual = thrust::equal_to> - __device__ const_iterator - find(CG g, Key const& k, Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}) const noexcept; + __device__ const_iterator find(CG const& g, + Key const& k, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}) const noexcept; /** * @brief Indicates whether the key `k` was inserted into the map. @@ -931,7 +990,7 @@ class static_reduction_map { template , typename KeyEqual = thrust::equal_to> - __device__ bool contains(CG g, + __device__ bool contains(CG const& g, Key const& k, Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}) noexcept; From fb9c0ecf7f09cf9d5f526f8315236656e476fa28 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Sun, 1 Aug 2021 21:09:07 +0000 Subject: [PATCH 34/69] Tests for static_reduction_map added. --- tests/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 40bd2b30a..45435b14e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -45,3 +45,8 @@ set(DYNAMIC_MAP_TEST_SRC ConfigureTest(DYNAMIC_MAP_TEST "${DYNAMIC_MAP_TEST_SRC}") #################################################################################################### +set(STATIC_REDUCTION_MAP_TEST_SRC + "${CMAKE_CURRENT_SOURCE_DIR}/static_reduction_map/static_reduction_map_test.cu") + +ConfigureTest(STATIC_REDUCTION_MAP_TEST "${STATIC_REDUCTION_MAP_TEST_SRC}") +#################################################################################################### From e8e54611c8909be966bfb7db526594fd30a370fc Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Sun, 1 Aug 2021 21:15:02 +0000 Subject: [PATCH 35/69] Benchmarks for static_reduction_map added + reduce-by-key performance comparison (cuco, Thrust, CUB). --- benchmarks/CMakeLists.txt | 48 ++++- .../hash_table/static_reduction_map_bench.cu | 202 +++++++++--------- benchmarks/key_generator.hpp | 164 ++++++++++++++ .../reduce_by_key/cub_reduce_by_key_bench.cu | 119 +++++++++++ .../reduce_by_key/cuco_reduce_by_key_bench.cu | 151 +++++++++++++ benchmarks/reduce_by_key/reduce_by_key.cu | 88 -------- .../thrust_reduce_by_key_bench.cu | 107 ++++++++++ benchmarks/util.hpp | 40 ++++ 8 files changed, 728 insertions(+), 191 deletions(-) create mode 100644 benchmarks/key_generator.hpp create mode 100644 benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu create mode 100644 benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu delete mode 100644 benchmarks/reduce_by_key/reduce_by_key.cu create mode 100644 benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu create mode 100644 benchmarks/util.hpp diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 467893be6..24932576c 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -12,10 +12,19 @@ CPMAddPackage( "RUN_HAVE_STD_REGEX 0" # ) -if (benchmark_ADDED) - # patch google benchmark target - set_target_properties(benchmark PROPERTIES CXX_STANDARD 14) -endif() +#if (benchmark_ADDED) +# # patch google benchmark target +# set_target_properties(benchmark PROPERTIES CXX_STANDARD 14) +#endif() + +CPMAddPackage( + NAME nvbench + GITHUB_REPOSITORY NVIDIA/nvbench + GIT_TAG main + GIT_SHALLOW TRUE +) + +set_target_properties(benchmark PROPERTIES CXX_STANDARD 17) ################################################################################################### # - compiler function ----------------------------------------------------------------------------- @@ -35,6 +44,22 @@ function(ConfigureBench BENCH_NAME BENCH_SRC) CUDA::cudart) endfunction(ConfigureBench) +################################################################################################### +function(ConfigureNVBench BENCH_NAME BENCH_SRC) + add_executable(${BENCH_NAME} "${BENCH_SRC}") + set_target_properties(${BENCH_NAME} PROPERTIES + POSITION_INDEPENDENT_CODE ON + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/nvbenchmarks") + target_include_directories(${BENCH_NAME} PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}") + #"${NVBench_SOURCE_DIR}") + target_compile_options(${BENCH_NAME} PRIVATE --expt-extended-lambda --expt-relaxed-constexpr) + target_link_libraries(${BENCH_NAME} PRIVATE + nvbench::main + pthread + cuco) +endfunction(ConfigureNVBench) + ################################################################################################### ### test sources ################################################################################## ################################################################################################### @@ -48,8 +73,17 @@ set(STATIC_MAP_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/hash_table/static_map_benc ConfigureBench(STATIC_MAP_BENCH "${STATIC_MAP_BENCH_SRC}") ################################################################################################### -ConfigureBench(STATIC_REDUCTION_MAP_BENCH "${CMAKE_CURRENT_SOURCE_DIR}/hash_table/static_reduction_map_bench.cu") +set(STATIC_REDUCTION_MAP_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/hash_table/static_reduction_map_bench.cu") +ConfigureNVBench(STATIC_REDUCTION_MAP_BENCH "${STATIC_REDUCTION_MAP_BENCH_SRC}") + +################################################################################################### +set(CUCO_RBK_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/reduce_by_key/cuco_reduce_by_key_bench.cu") +ConfigureNVBench(CUCO_RBK_BENCH "${CUCO_RBK_BENCH_SRC}") + +################################################################################################### +set(THRUST_RBK_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/reduce_by_key/thrust_reduce_by_key_bench.cu") +ConfigureNVBench(THRUST_RBK_BENCH "${THRUST_RBK_BENCH_SRC}") ################################################################################################### -set(RBK_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/reduce_by_key/reduce_by_key.cu") -ConfigureBench(RBK_BENCH "${RBK_BENCH_SRC}") +set(CUB_RBK_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/reduce_by_key/cub_reduce_by_key_bench.cu") +ConfigureNVBench(CUB_RBK_BENCH "${CUB_RBK_BENCH_SRC}") diff --git a/benchmarks/hash_table/static_reduction_map_bench.cu b/benchmarks/hash_table/static_reduction_map_bench.cu index 92a2ab788..ea6580554 100644 --- a/benchmarks/hash_table/static_reduction_map_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_bench.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,117 +14,127 @@ * limitations under the License. */ -#include -#include #include -#include -#include -#include -#include -#include "cuco/static_reduction_map.cuh" +#include +#include +#include +#include +#include -enum class dist_type { UNIQUE, UNIFORM, GAUSSIAN }; +/** + * @brief Enum representation for reduction operators + */ +enum class op_type { REDUCE_ADD, CUSTOM_OP }; + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + // Enum type: + op_type, + // Callable to generate input strings: + // Short identifier used for tables, command-line args, etc. + // Used when context is available to figure out the enum type. + [](op_type o) { + switch (o) { + case op_type::REDUCE_ADD: return "REDUCE_ADD"; + case op_type::CUSTOM_OP: return "CUSTOM_OP"; + default: return "ERROR"; + } + }, + // Callable to generate descriptions: + // If non-empty, these are used in `--list` to describe values. + // Used when context may not be available to figure out the type from the + // input string. + // Just use `[](auto) { return std::string{}; }` if you don't want these. + [](auto) { return std::string{}; }) -template -static void generate_keys(OutputIt output_begin, OutputIt output_end) -{ - auto num_keys = std::distance(output_begin, output_end); - - std::random_device rd; - std::mt19937 gen{rd()}; - - switch (Dist) { - case dist_type::UNIQUE: - for (auto i = 0; i < num_keys; ++i) { - output_begin[i] = i; - } - break; - case dist_type::UNIFORM: - for (auto i = 0; i < num_keys; ++i) { - output_begin[i] = std::abs(static_cast(gen())); - } - break; - case dist_type::GAUSSIAN: - std::normal_distribution<> dg{1e9, 1e7}; - for (auto i = 0; i < num_keys; ++i) { - output_begin[i] = std::abs(static_cast(dg(gen))); - } - break; - } -} +/** + * @brief Maps the enum value of a cuco reduction operator to its actual type + */ +template +struct op_type_map { +}; + +template <> +struct op_type_map { + template + using type = cuco::reduce_add; +}; + +template <> +struct op_type_map { + template + using type = cuco::custom_op>; // sum reduction with CAS loop +}; /** - * @brief Generates input sizes and hash table occupancies - * + * @brief A benchmark evaluating insert performance. */ -static void generate_size_and_occupancy(benchmark::internal::Benchmark* b) +template +void nvbench_cuco_static_reduction_map_insert( + nvbench::state& state, nvbench::type_list>) { - for (auto size = 4096; size <= 1 << 28; size *= 2) { - for (auto occupancy = 60; occupancy <= 60; occupancy += 10) { - b->Args({size, occupancy}); - } - } -} + using map_type = cuco::static_reduction_map::type, Key, Value>; -template typename ReductionOp> -static void BM_static_map_insert(::benchmark::State& state) -{ - using map_type = cuco::static_reduction_map, Key, Value>; + auto const num_elems = state.get_int64("NumInputs"); + auto const occupancy = state.get_float64("Occupancy"); + auto const dist = state.get_string("Distribution"); + auto const multiplicity = state.get_int64_or_default("Multiplicity", 8); - std::size_t num_keys = state.range(0); - float occupancy = state.range(1) / float{100}; - std::size_t size = num_keys / occupancy; + std::vector h_keys(num_elems); + std::vector h_values(num_elems); - std::vector h_keys(num_keys); - std::vector> h_pairs(num_keys); + generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); - generate_keys(h_keys.begin(), h_keys.end()); + // generate uniform random values + generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); + + // the size of the hash table under a given target occupancy depends on the + // number of unique keys in the input + std::size_t const unique = count_unique(h_keys.begin(), h_keys.end()); + std::size_t const capacity = std::ceil(SDIV(unique, occupancy)); + + // alternative occupancy calculation based on the total number of inputs + // std::size_t const capacity = num_elems / occupancy; thrust::device_vector d_keys(h_keys); - thrust::device_vector d_values(h_keys); + thrust::device_vector d_values(h_values); - auto pairs_begin = + auto d_pairs_begin = thrust::make_zip_iterator(thrust::make_tuple(d_keys.begin(), d_values.begin())); - auto pairs_end = pairs_begin + num_keys; + auto d_pairs_end = d_pairs_begin + num_elems; - for (auto _ : state) { - map_type map{size, -1}; - { - cuda_event_timer raii{state}; - map.insert(pairs_begin, pairs_end); - } - } + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, + [&](nvbench::launch& launch, auto& timer) { + map_type map{capacity, -1}; - state.SetBytesProcessed((sizeof(Key) + sizeof(Value)) * int64_t(state.iterations()) * - int64_t(state.range(0))); + timer.start(); + // TODO use CUDA stream provided by nvbench::launch + map.insert(d_pairs_begin, d_pairs_end); + timer.stop(); + }); } -BENCHMARK_TEMPLATE(BM_static_map_insert, int32_t, int32_t, dist_type::UNIQUE, cuco::reduce_add) - ->Unit(benchmark::kMillisecond) - ->UseManualTime() - ->Apply(generate_size_and_occupancy); - -BENCHMARK_TEMPLATE(BM_static_map_insert, int32_t, int32_t, dist_type::UNIFORM, cuco::reduce_add) - ->Unit(benchmark::kMillisecond) - ->UseManualTime() - ->Apply(generate_size_and_occupancy); - -BENCHMARK_TEMPLATE(BM_static_map_insert, int32_t, int32_t, dist_type::GAUSSIAN, cuco::reduce_add) - ->Unit(benchmark::kMillisecond) - ->UseManualTime() - ->Apply(generate_size_and_occupancy); - -BENCHMARK_TEMPLATE(BM_static_map_insert, int64_t, int64_t, dist_type::UNIQUE, cuco::reduce_add) - ->Unit(benchmark::kMillisecond) - ->UseManualTime() - ->Apply(generate_size_and_occupancy); - -BENCHMARK_TEMPLATE(BM_static_map_insert, int64_t, int64_t, dist_type::UNIFORM, cuco::reduce_add) - ->Unit(benchmark::kMillisecond) - ->UseManualTime() - ->Apply(generate_size_and_occupancy); - -BENCHMARK_TEMPLATE(BM_static_map_insert, int64_t, int64_t, dist_type::GAUSSIAN, cuco::reduce_add) - ->Unit(benchmark::kMillisecond) - ->UseManualTime() - ->Apply(generate_size_and_occupancy); \ No newline at end of file +// type parameter dimensions for benchmark +using key_type_range = nvbench::type_list; +using value_type_range = nvbench::type_list; +using op_type_range = nvbench::enum_type_list; + +// benchmark setups +NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_insert, + NVBENCH_TYPE_AXES(key_type_range, value_type_range, op_type_range)) + .set_name("cuco_static_reduction_map_insert_occupancy") + .set_type_axes_names({"Key", "Value", "ReductionOp"}) + .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_float64_axis("Occupancy", nvbench::range(0.5, 0.9, 0.1)) // occupancy range + .add_int64_axis("Multiplicity", {8}) // only applies to uniform distribution + .add_string_axis("Distribution", {"GAUSSIAN", "UNIFORM", "UNIQUE", "SAME"}); + +NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_insert, + NVBENCH_TYPE_AXES(key_type_range, value_type_range, op_type_range)) + .set_name("cuco_static_reduction_map_insert_multiplicity") + .set_type_axes_names({"Key", "Value", "ReductionOp"}) + .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_float64_axis("Occupancy", {0.8}) // fixed occupancy + .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000}) // key multiplicity range + .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file diff --git a/benchmarks/key_generator.hpp b/benchmarks/key_generator.hpp new file mode 100644 index 000000000..c16015866 --- /dev/null +++ b/benchmarks/key_generator.hpp @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2021, 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 +#include + +enum class dist_type { GAUSSIAN, GEOMETRIC, UNIFORM, UNIQUE, SAME }; + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + // Enum type: + dist_type, + // Callable to generate input strings: + // Short identifier used for tables, command-line args, etc. + // Used when context is available to figure out the enum type. + [](dist_type d) { + switch (d) { + case dist_type::GAUSSIAN: return "GAUSSIAN"; + case dist_type::GEOMETRIC: return "GEOMETRIC"; + case dist_type::UNIFORM: return "UNIFORM"; + case dist_type::UNIQUE: return "UNIQUE"; + case dist_type::SAME: return "SAME"; + default: return "ERROR"; + } + }, + // Callable to generate descriptions: + // If non-empty, these are used in `--list` to describe values. + // Used when context may not be available to figure out the type from the + // input string. + // Just use `[](auto) { return std::string{}; }` if you don't want these. + [](auto) { return std::string{}; }) + +template +static void generate_keys(nvbench::state& state, + dist_type dist, + OutputIt output_begin, + OutputIt output_end, + std::size_t multiplicity = 8) +{ + auto const num_keys = std::distance(output_begin, output_end); + + std::random_device rd; + std::mt19937 gen{rd()}; + + switch (dist) { + case dist_type::GAUSSIAN: { + auto const mean = static_cast(num_keys / 2); + auto const dev = static_cast(num_keys / 5); + + std::normal_distribution<> distribution{mean, dev}; + + for (auto i = 0; i < num_keys; ++i) { + auto k = distribution(gen); + while (k >= num_keys) { + k = distribution(gen); + } + output_begin[i] = k; + } + break; + } + case dist_type::GEOMETRIC: { + auto const max = std::numeric_limits::max(); + auto const coeff = static_cast(num_keys) / static_cast(max); + // Random sampling in range [0, INT32_MAX] + std::geometric_distribution distribution{1e-9}; + + for (auto i = 0; i < num_keys; ++i) { + output_begin[i] = distribution(gen) * coeff; + } + break; + } + case dist_type::UNIFORM: { + std::uniform_int_distribution distribution{1, static_cast(num_keys / multiplicity)}; + + for (auto i = 0; i < num_keys; ++i) { + output_begin[i] = distribution(gen); + } + break; + } + case dist_type::UNIQUE: { + // 3 because some HT implementations use 0, 1 as sentinels + for (auto i = 2; i < num_keys + 2; ++i) { + output_begin[i] = i; + } + std::random_shuffle(output_begin, output_end); + break; + } + case dist_type::SAME: { + std::fill(output_begin, output_end, Key(42)); + break; + } + default: { + state.skip("unknown distribution type"); + break; + } + } // switch +} + +template +static void generate_keys(nvbench::state& state, + std::string const& dist, + OutputIt output_begin, + OutputIt output_end, + std::size_t multiplicity = 8) +{ + dist_type enum_value{}; + + if (dist == "GAUSSIAN") { + enum_value = dist_type::GAUSSIAN; + } else if (dist == "GEOMETRIC") { + enum_value = dist_type::GEOMETRIC; + } else if (dist == "UNIFORM") { + enum_value = dist_type::UNIFORM; + } else if (dist == "UNIQUE") { + enum_value = dist_type::UNIQUE; + } else if (dist == "SAME") { + enum_value = dist_type::SAME; + } else { + state.skip("unknown distribution type"); + return; + } + + generate_keys(state, enum_value, output_begin, output_end, multiplicity); +} + +template +static void generate_prob_keys(double const matching_rate, + OutputIt output_begin, + OutputIt output_end) +{ + auto const num_keys = std::distance(output_begin, output_end); + auto const max = std::numeric_limits::max() - 2; + + std::random_device rd; + std::mt19937 gen{rd()}; + + std::uniform_real_distribution rate_dist(0.0, 1.0); + std::uniform_int_distribution non_match_dist{static_cast(num_keys + 2), max}; + + for (auto i = 0; i < num_keys; ++i) { + auto const tmp_rate = rate_dist(gen); + + if (tmp_rate > matching_rate) { output_begin[i] = non_match_dist(gen); } + } + + std::random_shuffle(output_begin, output_end); +} \ No newline at end of file diff --git a/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu new file mode 100644 index 000000000..0f481c158 --- /dev/null +++ b/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2021, 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 + +/** + * @brief A benchmark evaluating reduce-by-key performance. + */ +template +void nvbench_cub_reduce_by_key(nvbench::state& state, nvbench::type_list) +{ + auto const num_elems_in = state.get_int64("NumInputs"); + auto const dist = state.get_string("Distribution"); + auto const multiplicity = state.get_int64_or_default("Multiplicity", 8); + + std::vector h_keys(num_elems_in); + std::vector h_values(num_elems_in); + + generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); + + // generate uniform random values + generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); + + // double buffer (ying/yang) + thrust::device_vector d_keys_ying(h_keys); + thrust::device_vector d_values_ying(h_values); + + thrust::device_vector d_keys_yang(num_elems_in); + thrust::device_vector d_values_yang(num_elems_in); + + // CUB requires a dry-run in order to determine the size of required temp memory + std::size_t temp_bytes_sort = 0; + cub::DeviceRadixSort::SortPairs(nullptr, + temp_bytes_sort, + d_keys_ying.data().get(), + d_keys_yang.data().get(), + d_values_ying.data().get(), + d_values_yang.data().get(), + num_elems_in); + + thrust::device_vector d_num_elems_out(1); + + std::size_t temp_bytes_reduce = 0; + cub::DeviceReduce::ReduceByKey(nullptr, + temp_bytes_reduce, + d_keys_yang.data().get(), + d_keys_ying.data().get(), + d_values_yang.data().get(), + d_values_ying.data().get(), + d_num_elems_out.data().get(), + cub::Sum(), + num_elems_in); + + thrust::device_vector d_temp(std::max(temp_bytes_sort, temp_bytes_reduce)); + + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, + [&](nvbench::launch& launch, auto& timer) { + timer.start(); + cub::DeviceRadixSort::SortPairs(d_temp.data().get(), + temp_bytes_sort, + d_keys_ying.data().get(), + d_keys_yang.data().get(), + d_values_ying.data().get(), + d_values_yang.data().get(), + num_elems_in, + 0, + sizeof(Key) * 8, + launch.get_stream()); + + cub::DeviceReduce::ReduceByKey(d_temp.data().get(), + temp_bytes_reduce, + d_keys_yang.data().get(), + d_keys_ying.data().get(), + d_values_yang.data().get(), + d_values_ying.data().get(), + d_num_elems_out.data().get(), + cub::Sum(), + num_elems_in, + launch.get_stream()); + timer.stop(); + }); +} + +// type parameter dimensions for benchmark +using key_type_range = nvbench::type_list; +using value_type_range = nvbench::type_list; + +// benchmark setups +NVBENCH_BENCH_TYPES(nvbench_cub_reduce_by_key, NVBENCH_TYPE_AXES(key_type_range, value_type_range)) + .set_name("nvbench_cub_reduce_by_key_distribution") + .set_type_axes_names({"Key", "Value"}) + .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_int64_axis("Multiplicity", {8}) // only applies to uniform distribution + .add_string_axis("Distribution", {"GAUSSIAN", "UNIFORM", "UNIQUE", "SAME"}); + +NVBENCH_BENCH_TYPES(nvbench_cub_reduce_by_key, NVBENCH_TYPE_AXES(key_type_range, value_type_range)) + .set_name("nvbench_cub_reduce_by_key_multiplicity") + .set_type_axes_names({"Key", "Value"}) + .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000}) // key multiplicity range + .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file diff --git a/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu new file mode 100644 index 000000000..f67896833 --- /dev/null +++ b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2021, 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 + +/** + * @brief Enum representation for reduction operators + */ +enum class op_type { REDUCE_ADD, CUSTOM_OP }; + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + // Enum type: + op_type, + // Callable to generate input strings: + // Short identifier used for tables, command-line args, etc. + // Used when context is available to figure out the enum type. + [](op_type o) { + switch (o) { + case op_type::REDUCE_ADD: return "REDUCE_ADD"; + case op_type::CUSTOM_OP: return "CUSTOM_OP"; + default: return "ERROR"; + } + }, + // Callable to generate descriptions: + // If non-empty, these are used in `--list` to describe values. + // Used when context may not be available to figure out the type from the + // input string. + // Just use `[](auto) { return std::string{}; }` if you don't want these. + [](auto) { return std::string{}; }) + +/** + * @brief Maps the enum value of a cuco reduction operator to its actual type + */ +template +struct op_type_map {}; + +template <> +struct op_type_map { + template + using type = cuco::reduce_add; +}; + +template <> +struct op_type_map { + template + using type = cuco::custom_op>; // sum reduction with CAS loop +}; + +/** + * @brief A benchmark evaluating reduce-by-key performance. + */ +template < + typename Key, + typename Value, + op_type Op> +void nvbench_cuco_static_reduction_map_reduce_by_key( + nvbench::state& state, + nvbench::type_list< + Key, + Value, + nvbench::enum_type>) +{ + using map_type = + cuco::static_reduction_map::type, Key, Value>; + + auto const num_elems = state.get_int64("NumInputs"); + auto const occupancy = state.get_float64("Occupancy"); + auto const dist = state.get_string("Distribution"); + auto const multiplicity = state.get_int64_or_default("Multiplicity", 8); + + std::vector h_keys(num_elems); + std::vector h_values(num_elems); + + generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); + + // generate uniform random values + generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); + + // the size of the hash table under a given target occupancy depends on the + // number of unique keys in the input + std::size_t const unique = count_unique(h_keys.begin(), h_keys.end()); + std::size_t const capacity = std::ceil(SDIV(unique, occupancy)); + + // alternative occupancy calculation based on the total number of inputs + // std::size_t const capacity = num_elems / occupancy; + + thrust::device_vector d_keys(h_keys); + thrust::device_vector d_values(h_values); + + auto d_pairs_begin = + thrust::make_zip_iterator(thrust::make_tuple(d_keys.begin(), d_values.begin())); + auto d_pairs_end = d_pairs_begin + num_elems; + + state.exec( + nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { + map_type map{capacity, -1}; + + timer.start(); + // TODO use CUDA stream provided by nvbench::launch + map.insert(d_pairs_begin, d_pairs_end); + map.retrieve_all(d_keys.begin(), d_values.begin()); + timer.stop(); + }); +} + +// type parameter dimensions for benchmark +using key_type_range = nvbench::type_list; +using value_type_range = nvbench::type_list; +using op_type_range = nvbench::enum_type_list; + +NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_reduce_by_key, + NVBENCH_TYPE_AXES(key_type_range, + value_type_range, + op_type_range)) + .set_name("cuco_static_reduction_map_reduce_by_key_occupancy") + .set_type_axes_names({"Key", "Value", "ReductionOp"}) + .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_float64_axis("Occupancy", nvbench::range(0.5, 0.9, 0.1)) // occupancy range + .add_int64_axis("Multiplicity", {8}) // only applies to uniform distribution + .add_string_axis("Distribution", {"GAUSSIAN", "UNIFORM", "UNIQUE", "SAME"}); + +NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_reduce_by_key, + NVBENCH_TYPE_AXES(key_type_range, + value_type_range, + op_type_range)) + .set_name("cuco_static_reduction_map_reduce_by_key_multiplicity") + .set_type_axes_names({"Key", "Value", "ReductionOp"}) + .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_float64_axis("Occupancy", {0.8}) // fixed occupancy + .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000}) // key multiplicity range + .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file diff --git a/benchmarks/reduce_by_key/reduce_by_key.cu b/benchmarks/reduce_by_key/reduce_by_key.cu deleted file mode 100644 index 0ca08144f..000000000 --- a/benchmarks/reduce_by_key/reduce_by_key.cu +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2020, 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 - -/** - * @brief Generates input sizes and number of unique keys - * - */ -static void generate_size_and_num_unique(benchmark::internal::Benchmark* b) -{ - for (auto num_unique = 64; num_unique <= 1 << 20; num_unique <<= 1) { - for (auto size = 10'000'000; size <= 10'000'000; size *= 10) { - b->Args({size, num_unique}); - } - } -} - -template -void thrust_reduce_by_key(KeyRandomIterator keys_begin, - KeyRandomIterator keys_end, - ValueRandomIterator values_begin) -{ - using Key = typename thrust::iterator_traits::value_type; - using Value = typename thrust::iterator_traits::value_type; - - // Exact size of output is unknown (number of unique keys), but upper bounded - // by the number of keys - auto maximum_output_size = thrust::distance(keys_begin, keys_end); - thrust::device_vector output_keys(maximum_output_size); - thrust::device_vector output_values(maximum_output_size); - - thrust::sort_by_key(thrust::device, keys_begin, keys_end, values_begin); - thrust::reduce_by_key( - thrust::device, keys_begin, keys_end, values_begin, output_keys.begin(), output_values.end()); -} - -template -static void BM_thrust(::benchmark::State& state) -{ - auto const num_unique_keys = state.range(1); - for (auto _ : state) { - state.PauseTiming(); - thrust::device_vector keys(state.range(0)); - auto begin = thrust::make_counting_iterator(0); - thrust::transform( - begin, begin + state.range(0), keys.begin(), [num_unique_keys] __device__(auto i) { - return i % num_unique_keys; - }); - - thrust::device_vector values(state.range(0)); - state.ResumeTiming(); - thrust_reduce_by_key(keys.begin(), keys.end(), values.begin()); - cudaDeviceSynchronize(); - } -} -BENCHMARK_TEMPLATE(BM_thrust, int32_t, int32_t) - ->Unit(benchmark::kMillisecond) - ->Apply(generate_size_and_num_unique); - -BENCHMARK_TEMPLATE(BM_thrust, int64_t, int64_t) - ->Unit(benchmark::kMillisecond) - ->Apply(generate_size_and_num_unique); - -// TODO: Hash based reduce by key benchmark - - diff --git a/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu new file mode 100644 index 000000000..ad1c77058 --- /dev/null +++ b/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2021, 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 + +/** + * @brief Reduce-by-key implementation in Thrust. + */ +template +void thrust_reduce_by_key(KeyRandomIterator keys_begin, + KeyRandomIterator keys_end, + ValueRandomIterator values_begin) +{ + using Key = typename thrust::iterator_traits::value_type; + using Value = typename thrust::iterator_traits::value_type; + + // Exact size of output is unknown (number of unique keys), but upper-bounded + // by the number of keys + auto maximum_output_size = thrust::distance(keys_begin, keys_end); + thrust::device_vector output_keys(maximum_output_size); + thrust::device_vector output_values(maximum_output_size); + + thrust::sort_by_key(thrust::device, keys_begin, keys_end, values_begin); + thrust::reduce_by_key( + thrust::device, keys_begin, keys_end, values_begin, output_keys.begin(), output_values.begin()); +} + +/** + * @brief A benchmark evaluating reduce-by-key performance. + */ +template < + typename Key, + typename Value> +void nvbench_thrust_reduce_by_key( + nvbench::state& state, + nvbench::type_list< + Key, + Value>) +{ + auto const num_elems = state.get_int64("NumInputs"); + auto const dist = state.get_string("Distribution"); + auto const multiplicity = state.get_int64_or_default("Multiplicity", 8); + + std::vector h_keys(num_elems); + std::vector h_values(num_elems); + + generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); + + // generate uniform random values + generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); + + thrust::device_vector d_keys(h_keys); + thrust::device_vector d_values(h_values); + + state.exec( + nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { + timer.start(); + // TODO use CUDA stream provided by nvbench::launch + thrust_reduce_by_key(d_keys.begin(), d_keys.end(), d_values.begin()); + timer.stop(); + }); +} + +// type parameter dimensions for benchmark +using key_type_range = nvbench::type_list; +using value_type_range = nvbench::type_list; + +// benchmark setups +NVBENCH_BENCH_TYPES(nvbench_thrust_reduce_by_key, + NVBENCH_TYPE_AXES(key_type_range, + value_type_range)) + .set_name("nvbench_thrust_reduce_by_key_distribution") + .set_type_axes_names({"Key", "Value"}) + .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_int64_axis("Multiplicity", {8}) // only applies to uniform distribution + .add_string_axis("Distribution", {"GAUSSIAN", "UNIFORM", "UNIQUE", "SAME"}); + +NVBENCH_BENCH_TYPES(nvbench_thrust_reduce_by_key, + NVBENCH_TYPE_AXES(key_type_range, + value_type_range)) + .set_name("nvbench_thrust_reduce_by_key_multiplicity") + .set_type_axes_names({"Key", "Value"}) + .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000}) // key multiplicity range + .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file diff --git a/benchmarks/util.hpp b/benchmarks/util.hpp new file mode 100644 index 000000000..bfc115743 --- /dev/null +++ b/benchmarks/util.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021, 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 + +/** + * @brief Count the number of unique elements within a range + */ +template +std::size_t count_unique(Iter begin, Iter end) { + using value_type = typename std::iterator_traits::value_type; + + const auto size = std::distance(begin, end); + std::vector v(size); + std::copy(begin, end, v.begin()); + std::sort(v.begin(), v.end()); + + return std::distance(v.begin(), std::unique(v.begin(), v.end())); +} + +// safe division +#ifndef SDIV + #define SDIV(x,y)(((x)+(y)-1)/(y)) +#endif \ No newline at end of file From 1d97a6fed59f7bdcf7bb926e44bb128904550207 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Sun, 1 Aug 2021 22:32:45 +0000 Subject: [PATCH 36/69] Added CUDA stream support for static_reduction_map. --- .../hash_table/static_reduction_map_bench.cu | 3 +- .../reduce_by_key/cuco_reduce_by_key_bench.cu | 66 ++++++++---------- include/cuco/detail/static_reduction_map.inl | 68 ++++++++++++------- include/cuco/static_reduction_map.cuh | 40 +++++++---- 4 files changed, 97 insertions(+), 80 deletions(-) diff --git a/benchmarks/hash_table/static_reduction_map_bench.cu b/benchmarks/hash_table/static_reduction_map_bench.cu index ea6580554..93eac5aed 100644 --- a/benchmarks/hash_table/static_reduction_map_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_bench.cu @@ -107,8 +107,7 @@ void nvbench_cuco_static_reduction_map_insert( map_type map{capacity, -1}; timer.start(); - // TODO use CUDA stream provided by nvbench::launch - map.insert(d_pairs_begin, d_pairs_end); + map.insert(d_pairs_begin, d_pairs_end, launch.get_stream()); timer.stop(); }); } diff --git a/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu index f67896833..e5ee1a7a2 100644 --- a/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu +++ b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu @@ -16,10 +16,10 @@ #include #include -#include +#include #include +#include #include -#include /** * @brief Enum representation for reduction operators @@ -35,7 +35,7 @@ NVBENCH_DECLARE_ENUM_TYPE_STRINGS( [](op_type o) { switch (o) { case op_type::REDUCE_ADD: return "REDUCE_ADD"; - case op_type::CUSTOM_OP: return "CUSTOM_OP"; + case op_type::CUSTOM_OP: return "CUSTOM_OP"; default: return "ERROR"; } }, @@ -50,7 +50,8 @@ NVBENCH_DECLARE_ENUM_TYPE_STRINGS( * @brief Maps the enum value of a cuco reduction operator to its actual type */ template -struct op_type_map {}; +struct op_type_map { +}; template <> struct op_type_map { @@ -61,25 +62,17 @@ struct op_type_map { template <> struct op_type_map { template - using type = cuco::custom_op>; // sum reduction with CAS loop + using type = cuco::custom_op>; // sum reduction with CAS loop }; /** * @brief A benchmark evaluating reduce-by-key performance. */ -template < - typename Key, - typename Value, - op_type Op> +template void nvbench_cuco_static_reduction_map_reduce_by_key( - nvbench::state& state, - nvbench::type_list< - Key, - Value, - nvbench::enum_type>) + nvbench::state& state, nvbench::type_list>) { - using map_type = - cuco::static_reduction_map::type, Key, Value>; + using map_type = cuco::static_reduction_map::type, Key, Value>; auto const num_elems = state.get_int64("NumInputs"); auto const occupancy = state.get_float64("Occupancy"); @@ -102,50 +95,45 @@ void nvbench_cuco_static_reduction_map_reduce_by_key( // alternative occupancy calculation based on the total number of inputs // std::size_t const capacity = num_elems / occupancy; - thrust::device_vector d_keys(h_keys); + thrust::device_vector d_keys(h_keys); thrust::device_vector d_values(h_values); auto d_pairs_begin = thrust::make_zip_iterator(thrust::make_tuple(d_keys.begin(), d_values.begin())); auto d_pairs_end = d_pairs_begin + num_elems; - state.exec( - nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { - map_type map{capacity, -1}; + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, + [&](nvbench::launch& launch, auto& timer) { + map_type map{capacity, -1}; - timer.start(); - // TODO use CUDA stream provided by nvbench::launch - map.insert(d_pairs_begin, d_pairs_end); - map.retrieve_all(d_keys.begin(), d_values.begin()); - timer.stop(); - }); + timer.start(); + map.insert(d_pairs_begin, d_pairs_end, launch.get_stream()); + map.retrieve_all(d_keys.begin(), d_values.begin(), launch.get_stream()); + timer.stop(); + }); } // type parameter dimensions for benchmark using key_type_range = nvbench::type_list; using value_type_range = nvbench::type_list; -using op_type_range = nvbench::enum_type_list; +using op_type_range = nvbench::enum_type_list; NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_reduce_by_key, - NVBENCH_TYPE_AXES(key_type_range, - value_type_range, - op_type_range)) + NVBENCH_TYPE_AXES(key_type_range, value_type_range, op_type_range)) .set_name("cuco_static_reduction_map_reduce_by_key_occupancy") .set_type_axes_names({"Key", "Value", "ReductionOp"}) .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. - .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs - .add_float64_axis("Occupancy", nvbench::range(0.5, 0.9, 0.1)) // occupancy range - .add_int64_axis("Multiplicity", {8}) // only applies to uniform distribution + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_float64_axis("Occupancy", nvbench::range(0.5, 0.9, 0.1)) // occupancy range + .add_int64_axis("Multiplicity", {8}) // only applies to uniform distribution .add_string_axis("Distribution", {"GAUSSIAN", "UNIFORM", "UNIQUE", "SAME"}); NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_reduce_by_key, - NVBENCH_TYPE_AXES(key_type_range, - value_type_range, - op_type_range)) + NVBENCH_TYPE_AXES(key_type_range, value_type_range, op_type_range)) .set_name("cuco_static_reduction_map_reduce_by_key_multiplicity") .set_type_axes_names({"Key", "Value", "ReductionOp"}) .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. - .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs - .add_float64_axis("Occupancy", {0.8}) // fixed occupancy - .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000}) // key multiplicity range + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_float64_axis("Occupancy", {0.8}) // fixed occupancy + .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000}) // key multiplicity range .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index dcb385ae0..7ec7a676e 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -39,7 +39,8 @@ static_reduction_map::static_reductio empty_key_sentinel_{empty_key_sentinel}, empty_value_sentinel_{ReductionOp::identity}, op_{reduction_op}, - slot_allocator_{alloc} + slot_allocator_{alloc}, + counter_allocator_{alloc} { slots_ = std::allocator_traits::allocate(slot_allocator_, capacity_); @@ -48,8 +49,6 @@ static_reduction_map::static_reductio auto const grid_size = (capacity_ + stride * block_size - 1) / (stride * block_size); detail::initialize<<>>( slots_, get_empty_key_sentinel(), get_empty_value_sentinel(), get_capacity()); - - CUCO_CUDA_TRY(cudaMallocManaged(&num_successes_, sizeof(atomic_ctr_type))); } template ::~static_reduction_map() { std::allocator_traits::deallocate(slot_allocator_, slots_, capacity_); - CUCO_ASSERT_CUDA_SUCCESS(cudaFree(num_successes_)); } template template -void static_reduction_map::insert(InputIt first, - InputIt last, - Hash hash, - KeyEqual key_equal) +void static_reduction_map::insert( + InputIt first, InputIt last, cudaStream_t stream, Hash hash, KeyEqual key_equal) { auto num_keys = std::distance(first, last); if (num_keys == 0) { return; } @@ -83,16 +79,29 @@ void static_reduction_map::insert(Inp auto const grid_size = (tile_size * num_keys + stride * block_size - 1) / (stride * block_size); auto view = get_device_mutable_view(); - *num_successes_ = 0; - int device_id; - CUCO_CUDA_TRY(cudaGetDevice(&device_id)); - CUCO_CUDA_TRY(cudaMemPrefetchAsync(num_successes_, sizeof(atomic_ctr_type), device_id)); + atomic_ctr_type *h_num_successes, *d_num_successes; + CUCO_CUDA_TRY(cudaMallocHost(&h_num_successes, sizeof(atomic_ctr_type))); + + auto tmp_counter_allocator = counter_allocator_; + d_num_successes = + std::allocator_traits::allocate(tmp_counter_allocator, 1); + + h_num_successes->store(static_cast(0), cuda::std::memory_order_relaxed); + CUCO_CUDA_TRY(cudaMemcpyAsync( + d_num_successes, h_num_successes, sizeof(atomic_ctr_type), cudaMemcpyHostToDevice, stream)); + + detail::insert<<>>( + first, first + num_keys, d_num_successes, view, hash, key_equal); + + CUCO_CUDA_TRY(cudaMemcpyAsync( + h_num_successes, d_num_successes, sizeof(atomic_ctr_type), cudaMemcpyDeviceToHost, stream)); + CUCO_CUDA_TRY(cudaStreamSynchronize(stream)); - detail::insert - <<>>(first, first + num_keys, num_successes_, view, hash, key_equal); - CUCO_CUDA_TRY(cudaDeviceSynchronize()); + size_ += h_num_successes->load(cuda::std::memory_order_relaxed); - size_ += num_successes_->load(cuda::std::memory_order_relaxed); + CUCO_CUDA_TRY(cudaFreeHost(h_num_successes)); + std::allocator_traits::deallocate( + tmp_counter_allocator, d_num_successes, 1); } template template -void static_reduction_map::find( - InputIt first, InputIt last, OutputIt output_begin, Hash hash, KeyEqual key_equal) +void static_reduction_map::find(InputIt first, + InputIt last, + OutputIt output_begin, + cudaStream_t stream, + Hash hash, + KeyEqual key_equal) { auto num_keys = std::distance(first, last); if (num_keys == 0) { return; } @@ -114,8 +127,8 @@ void static_reduction_map::find( auto view = get_device_view(); detail::find - <<>>(first, last, output_begin, view, hash, key_equal); - CUCO_CUDA_TRY(cudaDeviceSynchronize()); + <<>>(first, last, output_begin, view, hash, key_equal); + CUCO_CUDA_TRY(cudaStreamSynchronize(stream)); } namespace detail { @@ -146,7 +159,7 @@ template template void static_reduction_map::retrieve_all( - KeyOut keys_out, ValueOut values_out) + KeyOut keys_out, ValueOut values_out, cudaStream_t stream) { // Convert pair_type to thrust::tuple to allow assigning to a zip iterator auto begin = @@ -155,7 +168,7 @@ void static_reduction_map::retrieve_a auto filled = detail::slot_is_filled{get_empty_key_sentinel()}; auto zipped_out = thrust::make_zip_iterator(thrust::make_tuple(keys_out, values_out)); - thrust::copy_if(thrust::device, begin, end, zipped_out, filled); + thrust::copy_if(thrust::cuda::par.on(stream), begin, end, zipped_out, filled); } template template void static_reduction_map::contains( - InputIt first, InputIt last, OutputIt output_begin, Hash hash, KeyEqual key_equal) + InputIt first, + InputIt last, + OutputIt output_begin, + cudaStream_t stream, + Hash hash, + KeyEqual key_equal) { auto num_keys = std::distance(first, last); if (num_keys == 0) { return; } @@ -177,8 +195,8 @@ void static_reduction_map::contains( auto view = get_device_view(); detail::contains - <<>>(first, last, output_begin, view, hash, key_equal); - CUCO_CUDA_TRY(cudaDeviceSynchronize()); + <<>>(first, last, output_begin, view, hash, key_equal); + CUCO_CUDA_TRY(cudaStreamSynchronize(stream)); } template ::rebind_alloc; + using counter_allocator_type = + typename std::allocator_traits::rebind_alloc; #if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 700) static_assert(atomic_key_type::is_always_lock_free, @@ -282,13 +284,18 @@ class static_reduction_map { * @tparam KeyEqual Binary callable type * @param first Beginning of the sequence of key/value pairs * @param last End of the sequence of key/value pairs + * @param stream CUDA stream used for insert * @param hash The unary function to apply to hash each key * @param key_equal The binary function to compare two keys for equality */ template , typename KeyEqual = thrust::equal_to> - void insert(InputIt first, InputIt last, Hash hash = Hash{}, KeyEqual key_equal = KeyEqual{}); + void insert(InputIt first, + InputIt last, + cudaStream_t stream = 0, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}); /** * @brief Finds the values corresponding to all keys in the range `[first, last)`. @@ -305,6 +312,7 @@ class static_reduction_map { * @param first Beginning of the sequence of keys * @param last End of the sequence of keys * @param output_begin Beginning of the sequence of values retrieved for each key + * @param stream CUDA stream used for this operation * @param hash The unary function to apply to hash each key * @param key_equal The binary function to compare two keys for equality */ @@ -315,8 +323,9 @@ class static_reduction_map { void find(InputIt first, InputIt last, OutputIt output_begin, - Hash hash = Hash{}, - KeyEqual key_equal = KeyEqual{}); + cudaStream_t stream = 0, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}); /** * @brief Retrieves all of the keys and their associated values. @@ -333,9 +342,10 @@ class static_reduction_map { * convertible from `mapped_type`. * @param keys_out Beginning output iterator for keys * @param values_out Beginning output iterator for values + * @param stream CUDA stream used for this operation */ template - void retrieve_all(KeyOut keys_out, ValueOut values_out); + void retrieve_all(KeyOut keys_out, ValueOut values_out, cudaStream_t stream = 0); /** * @brief Indicates whether the keys in the range `[first, last)` are contained in the map. @@ -351,6 +361,7 @@ class static_reduction_map { * @param first Beginning of the sequence of keys * @param last End of the sequence of keys * @param output_begin Beginning of the sequence of booleans for the presence of each key + * @param stream CUDA stream used * @param hash The unary function to apply to hash each key * @param key_equal The binary function to compare two keys for equality */ @@ -361,8 +372,9 @@ class static_reduction_map { void contains(InputIt first, InputIt last, OutputIt output_begin, - Hash hash = Hash{}, - KeyEqual key_equal = KeyEqual{}); + cudaStream_t stream = 0, + Hash hash = Hash{}, + KeyEqual key_equal = KeyEqual{}); private: class device_view_base { @@ -1067,14 +1079,14 @@ class static_reduction_map { value_type const* raw_slots_end() const noexcept { return raw_slots_begin() + get_capacity(); } - pair_atomic_type* slots_{nullptr}; ///< Pointer to flat slots storage - std::size_t capacity_{}; ///< Total number of slots - std::size_t size_{}; ///< Number of keys in map - Key empty_key_sentinel_{}; ///< Key value that represents an empty slot - Value empty_value_sentinel_{}; ///< Initial value of empty slot - atomic_ctr_type* num_successes_{}; ///< Number of successfully inserted keys on insert - ReductionOp op_{}; ///< Binary operation reduction function object - slot_allocator_type slot_allocator_{}; ///< Allocator used to allocate slots + pair_atomic_type* slots_{nullptr}; ///< Pointer to flat slots storage + std::size_t capacity_{}; ///< Total number of slots + std::size_t size_{}; ///< Number of keys in map + Key empty_key_sentinel_{}; ///< Key value that represents an empty slot + Value empty_value_sentinel_{}; ///< Initial value of empty slot + ReductionOp op_{}; ///< Binary operation reduction function object + slot_allocator_type slot_allocator_{}; ///< Allocator used to allocate slots + counter_allocator_type counter_allocator_{}; ///< Allocator used to allocate counters }; } // namespace cuco From b4351fc24affe048becc40bab34e4d1f6feb0b65 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Mon, 2 Aug 2021 02:12:33 +0000 Subject: [PATCH 37/69] Fix custom reduction op implementation and add exponential backoff strategy. --- include/cuco/static_reduction_map.cuh | 51 +++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index cde8cc6ec..06b9ce454 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -38,6 +38,7 @@ #include #endif +#include #include #include #include @@ -46,6 +47,12 @@ namespace cuco { +/** + * @brief `+` reduction functor that internally uses an atomic fetch-and-add + * operation. + * + * @tparam T The data type used for reduction + */ template struct reduce_add { using value_type = T; @@ -58,6 +65,12 @@ struct reduce_add { } }; +/** + * @brief `-` reduction functor that internally uses an atomic fetch-and-add + * operation. + * + * @tparam T The data type used for reduction + */ template struct reduce_sub { using value_type = T; @@ -70,6 +83,12 @@ struct reduce_sub { } }; +/** + * @brief `min` reduction functor that internally uses an atomic fetch-and-add + * operation. + * + * @tparam T The data type used for reduction + */ template struct reduce_min { using value_type = T; @@ -82,6 +101,12 @@ struct reduce_min { } }; +/** + * @brief `max` reduction functor that internally uses an atomic fetch-and-add + * operation. + * + * @tparam T The data type used for reduction + */ template struct reduce_max { using value_type = T; @@ -94,7 +119,19 @@ struct reduce_max { } }; -template +/** + * @brief Wrapper for a user-defined custom reduction operator. + * @brief Internally uses an atomic compare-and-swap loop. + * + * @tparam T The data type used for reduction + * @tparam Identity Neutral element under the given reduction group + * @tparam Op Commutative and associative binary operator + */ +template struct custom_op { using value_type = T; static constexpr T identity = Identity; @@ -104,8 +141,18 @@ struct custom_op { template __device__ T apply(cuda::atomic& slot, T2 const& value) const { + [[maybe_unused]] unsigned ns = BackoffBaseDelay; + auto old = slot.load(cuda::memory_order_relaxed); - while (not slot.compare_exchange_strong(old, op(old, value), cuda::memory_order_relaxed)) {} + while (not slot.compare_exchange_strong(old, op(old, value), cuda::memory_order_relaxed)) { +#if __CUDA_ARCH__ >= 700 + // exponential backoff strategy to reduce atomic contention + if (true) { + asm volatile("nanosleep.u32 %0;" ::"r"((unsigned)ns) :); + if (ns < BackoffMaxDelay) { ns *= 2; } + } +#endif + } return old; } }; From 80ef0eef34805a28b8253310f0a7f43daaa20188 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Mon, 2 Aug 2021 02:14:12 +0000 Subject: [PATCH 38/69] Parameter grid search for CAS loop backoff added. --- benchmarks/CMakeLists.txt | 4 + .../hash_table/static_reduction_map_bench.cu | 72 ++++++++++++++ .../static_reduction_map_param_grid_search.cu | 97 +++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 benchmarks/hash_table/static_reduction_map_param_grid_search.cu diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 24932576c..5f16ca5bf 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -87,3 +87,7 @@ ConfigureNVBench(THRUST_RBK_BENCH "${THRUST_RBK_BENCH_SRC}") ################################################################################################### set(CUB_RBK_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/reduce_by_key/cub_reduce_by_key_bench.cu") ConfigureNVBench(CUB_RBK_BENCH "${CUB_RBK_BENCH_SRC}") + +################################################################################################### +set(STATIC_REDUCTION_MAP_PARAM_GRID_SEARCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/hash_table/static_reduction_map_param_grid_search.cu") +ConfigureNVBench(STATIC_REDUCTION_MAP_PARAM_GRID_SEARCH "${STATIC_REDUCTION_MAP_PARAM_GRID_SEARCH_SRC}") \ No newline at end of file diff --git a/benchmarks/hash_table/static_reduction_map_bench.cu b/benchmarks/hash_table/static_reduction_map_bench.cu index 93eac5aed..0d651139c 100644 --- a/benchmarks/hash_table/static_reduction_map_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_bench.cu @@ -112,10 +112,68 @@ void nvbench_cuco_static_reduction_map_insert( }); } +/** + * @brief A benchmark evaluating insert performance. + */ +template +void nvbench_cuco_static_reduction_map_custom_op_insert( + nvbench::state& state, + nvbench::type_list, + nvbench::enum_type>) +{ + using custom_op_type = + cuco::custom_op, BackoffBaseDelay, BackoffMaxDelay>; + using map_type = cuco::static_reduction_map; + + auto const num_elems = state.get_int64("NumInputs"); + auto const occupancy = state.get_float64("Occupancy"); + auto const dist = state.get_string("Distribution"); + auto const multiplicity = state.get_int64_or_default("Multiplicity", 8); + + std::vector h_keys(num_elems); + std::vector h_values(num_elems); + + generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); + + // generate uniform random values + generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); + + // the size of the hash table under a given target occupancy depends on the + // number of unique keys in the input + std::size_t const unique = count_unique(h_keys.begin(), h_keys.end()); + std::size_t const capacity = std::ceil(SDIV(unique, occupancy)); + + // alternative occupancy calculation based on the total number of inputs + // std::size_t const capacity = num_elems / occupancy; + + thrust::device_vector d_keys(h_keys); + thrust::device_vector d_values(h_values); + + auto d_pairs_begin = + thrust::make_zip_iterator(thrust::make_tuple(d_keys.begin(), d_values.begin())); + auto d_pairs_end = d_pairs_begin + num_elems; + + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, + [&](nvbench::launch& launch, auto& timer) { + map_type map{capacity, -1}; + + timer.start(); + map.insert(d_pairs_begin, d_pairs_end, launch.get_stream()); + timer.stop(); + }); +} + // type parameter dimensions for benchmark using key_type_range = nvbench::type_list; using value_type_range = nvbench::type_list; using op_type_range = nvbench::enum_type_list; +using base_delay_range = nvbench::enum_type_list<0, 8, 16, 32, 64, 128, 256>; +using max_delay_range = nvbench::enum_type_list<2048, 4096, 8192>; // benchmark setups NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_insert, @@ -136,4 +194,18 @@ NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_insert, .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs .add_float64_axis("Occupancy", {0.8}) // fixed occupancy .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000}) // key multiplicity range + .add_string_axis("Distribution", {"UNIFORM"}); + +NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_custom_op_insert, + NVBENCH_TYPE_AXES(nvbench::type_list, + nvbench::type_list, + base_delay_range, + max_delay_range)) + .set_name("cuco_static_reduction_map_custom_op_insert_contention") + .set_type_axes_names({"Key", "Value", "BackoffBaseDelay", "BackoffMaxDelay"}) + .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_float64_axis("Occupancy", {0.8}) // fixed occupancy + .add_int64_axis("Multiplicity", + {1, 10, 100, 1'000, 10'000, 100'000, 200'000}) // key multiplicity range .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file diff --git a/benchmarks/hash_table/static_reduction_map_param_grid_search.cu b/benchmarks/hash_table/static_reduction_map_param_grid_search.cu new file mode 100644 index 000000000..41baaa872 --- /dev/null +++ b/benchmarks/hash_table/static_reduction_map_param_grid_search.cu @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2021, 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 + +/** + * @brief Grid search evaluating backoff delay params for cuco::custom_op + */ +template +void nvbench_cuco_static_reduction_map_custom_op_backoff_delay( + nvbench::state& state, + nvbench::type_list, + nvbench::enum_type>) +{ + using custom_op_type = + cuco::custom_op, BackoffBaseDelay, BackoffMaxDelay>; + using map_type = cuco::static_reduction_map; + + auto const num_elems = state.get_int64("NumInputs"); + auto const occupancy = state.get_float64("Occupancy"); + auto const dist = state.get_string("Distribution"); + auto const multiplicity = state.get_int64_or_default("Multiplicity", 8); + + std::vector h_keys(num_elems); + std::vector h_values(num_elems); + + generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); + + // generate uniform random values + generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); + + // the size of the hash table under a given target occupancy depends on the + // number of unique keys in the input + std::size_t const unique = count_unique(h_keys.begin(), h_keys.end()); + std::size_t const capacity = std::ceil(SDIV(unique, occupancy)); + + // alternative occupancy calculation based on the total number of inputs + // std::size_t const capacity = num_elems / occupancy; + + thrust::device_vector d_keys(h_keys); + thrust::device_vector d_values(h_values); + + auto d_pairs_begin = + thrust::make_zip_iterator(thrust::make_tuple(d_keys.begin(), d_values.begin())); + auto d_pairs_end = d_pairs_begin + num_elems; + + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, + [&](nvbench::launch& launch, auto& timer) { + map_type map{capacity, -1}; + + timer.start(); + map.insert(d_pairs_begin, d_pairs_end, launch.get_stream()); + timer.stop(); + }); +} + +// type parameter dimensions for benchmark +using key_type_range = nvbench::type_list; +using value_type_range = nvbench::type_list; +using base_delay_range = nvbench::enum_type_list<4, 8, 16, 32, 64, 128, 256, 512>; +using max_delay_range = nvbench::enum_type_list<2'048, 4'096, 8'192, 16'384>; + +// benchmark setups +NVBENCH_BENCH_TYPES( + nvbench_cuco_static_reduction_map_custom_op_backoff_delay, + NVBENCH_TYPE_AXES(key_type_range, value_type_range, base_delay_range, max_delay_range)) + .set_name("cuco_static_reduction_map_custom_op_backoff_delay") + .set_type_axes_names({"Key", "Value", "BackoffBaseDelay", "BackoffMaxDelay"}) + .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_float64_axis("Occupancy", {0.8}) // fixed occupancy + .add_int64_axis("Multiplicity", + {1, 10, 100, 1'000, 10'000, 100'000, 1'000'000}) // key multiplicity range + .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file From 54e2022c91615c807583c5a38db881c574299e59 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Wed, 4 Aug 2021 21:09:19 +0000 Subject: [PATCH 39/69] Reduce-by-key performance analysis. --- benchmarks/analysis/notebooks/rbk_bench.ipynb | 716 ++++++++++++++++++ .../reduce_by_key/cub_reduce_by_key_bench.cu | 3 +- .../reduce_by_key/cuco_reduce_by_key_bench.cu | 7 +- .../thrust_reduce_by_key_bench.cu | 50 +- 4 files changed, 743 insertions(+), 33 deletions(-) create mode 100644 benchmarks/analysis/notebooks/rbk_bench.ipynb diff --git a/benchmarks/analysis/notebooks/rbk_bench.ipynb b/benchmarks/analysis/notebooks/rbk_bench.ipynb new file mode 100644 index 000000000..82dd5c5e9 --- /dev/null +++ b/benchmarks/analysis/notebooks/rbk_bench.ipynb @@ -0,0 +1,716 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Preparation" + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 1, + "source": [ + "!pip3 install pandas\n", + "!pip3 install matplotlib\n", + "\n", + "# Import libraries\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib\n", + "from collections import namedtuple\n", + "\n", + "#plt.style.use('seaborn-white')" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Requirement already satisfied: pandas in /home/djuenger/miniconda3/lib/python3.9/site-packages (1.3.1)\n", + "Requirement already satisfied: numpy>=1.17.3 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from pandas) (1.21.1)\n", + "Requirement already satisfied: python-dateutil>=2.7.3 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from pandas) (2.8.2)\n", + "Requirement already satisfied: pytz>=2017.3 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from pandas) (2021.1)\n", + "Requirement already satisfied: six>=1.5 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from python-dateutil>=2.7.3->pandas) (1.16.0)\n", + "Requirement already satisfied: matplotlib in /home/djuenger/miniconda3/lib/python3.9/site-packages (3.4.2)\n", + "Requirement already satisfied: pillow>=6.2.0 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (8.3.1)\n", + "Requirement already satisfied: cycler>=0.10 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (0.10.0)\n", + "Requirement already satisfied: pyparsing>=2.2.1 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (2.4.7)\n", + "Requirement already satisfied: kiwisolver>=1.0.1 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (1.3.1)\n", + "Requirement already satisfied: numpy>=1.16 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (1.21.1)\n", + "Requirement already satisfied: python-dateutil>=2.7 in /home/djuenger/miniconda3/lib/python3.9/site-packages (from matplotlib) (2.8.2)\n", + "Requirement already satisfied: six in /home/djuenger/miniconda3/lib/python3.9/site-packages (from cycler>=0.10->matplotlib) (1.16.0)\n" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 2, + "source": [ + "# helper functions\n", + "\n", + "style_ = namedtuple(\"style_\", [\"color\", \"marker\", \"linestyle\"])\n", + "\n", + "styles = {\n", + " \"THRUST\" : style_('r', 'v', '-'),\n", + " \"CUB\" : style_('b', 'o', '-'),\n", + " \"CUCO\" : style_('g', 'x', '-'),\n", + " \"CUCO α=50%\" : style_('g', 'x', '-'),\n", + " \"CUCO α=80%\" : style_('g', 'x', '--')}\n", + "\n", + "def load_csv_files(csv_files):\n", + " dfs = {}\n", + " for key, fname in csv_files.items():\n", + " df = pd.read_csv(fname)\n", + " dfs[key] = df[df[\"Skipped\"] == \"No\"]\n", + " return dfs\n", + "\n", + "def filter_bench(dfs, query):\n", + " if isinstance(dfs, dict):\n", + " filtered_dfs = {}\n", + " for key in dfs.keys():\n", + " filtered_dfs[key] = dfs[key].query(query)\n", + " return filtered_dfs\n", + " else:\n", + " return dfs.query(query)\n", + "\n", + "def plot_bench(dfs, xlabel, show_legend=True, title=None, ofname=None, show_xlabel=True, show_ylabel=True, log_xscale=False, log_yscale=False, styles=styles, font_size=14):\n", + " fig, ax = plt.subplots(1, 1)\n", + "\n", + " ax.tick_params(labelsize=font_size)\n", + " if(show_ylabel):\n", + " ax.set_xlabel(xlabel, fontsize=font_size)\n", + " if(show_ylabel):\n", + " ax.set_ylabel(\"Operations per second\", fontsize=font_size)\n", + " if(log_xscale):\n", + " ax.set_xscale('log')\n", + " if(log_yscale):\n", + " ax.set_yscale('log')\n", + " ax.set_title(title, fontsize=font_size)\n", + " ax.grid()\n", + "\n", + " for key, df in dfs.items(): \n", + " style = styles[key]\n", + "\n", + " Y = df[\"NumInputs\"].unique()[0]/df[\"GPU Time (sec)\"]\n", + "\n", + " if xlabel in df.columns:\n", + " X = df[xlabel]\n", + " \n", + " ax.plot(X, Y, label=key, color=style.color, marker=style.marker, linestyle=style.linestyle)\n", + " ax.scatter(X, Y, color=style.color, marker=style.marker, linestyle=style.linestyle)\n", + " else:\n", + " ax.axhline(y=Y.iloc[0], label=key, color=style.color, linestyle=style.linestyle)\n", + "\n", + " if(show_legend):\n", + " plt.legend(fontsize=font_size - 4)\n", + "\n", + " if(ofname):\n", + " plt.savefig(ofname, dpi=1200, format='pdf', bbox_inches='tight')\n", + "\n", + " plt.show()" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 3, + "source": [ + "v100_dfs = load_csv_files({\n", + " \"CUCO\" : \"../results/cuco_rbk_v100.csv\",\n", + " \"CUB\" : \"../results/cub_rbk_v100.csv\",\n", + " \"THRUST\" : \"../results/thrust_rbk_v100.csv\"})\n", + "\n", + "a100_dfs = load_csv_files({\n", + " \"CUCO\" : \"../results/cuco_rbk_a100.csv\",\n", + " \"CUB\" : \"../results/cub_rbk_a100.csv\",\n", + " \"THRUST\" : \"../results/thrust_rbk_a100.csv\"})" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 4, + "source": [ + "# for CUCO, show distinct traces for load factors of 50% and 80%, respectively\n", + "query = 'Distribution == \"UNIFORM\" and\\\n", + " Benchmark.str.contains(\"multiplicity\")'\n", + "\n", + "v100_dfs_mult = filter_bench(v100_dfs, query)\n", + "v100_dfs_mult['CUCO α=50%'] = filter_bench(v100_dfs_mult['CUCO'], 'Occupancy == 0.5')\n", + "v100_dfs_mult['CUCO α=80%'] = filter_bench(v100_dfs_mult['CUCO'], 'Occupancy == 0.8')\n", + "del v100_dfs_mult['CUCO']\n", + "\n", + "a100_dfs_mult = filter_bench(a100_dfs, query)\n", + "a100_dfs_mult['CUCO α=50%'] = filter_bench(a100_dfs_mult['CUCO'], 'Occupancy == 0.5')\n", + "a100_dfs_mult['CUCO α=80%'] = filter_bench(a100_dfs_mult['CUCO'], 'Occupancy == 0.8')\n", + "del a100_dfs_mult['CUCO']\n", + "\n", + "\n", + "#### RBK\n", + "### V100\n", + "## Multiplicity\n", + "# I32/I32\n", + "print(\"V100 I32/I32 UNIFORM\")\n", + "query = 'Key == \"I32\" and\\\n", + " Value == \"I32\"'\n", + "plot_bench(filter_bench(v100_dfs_mult, query), \"Multiplicity\", log_xscale=True)\n", + "\n", + "# I64/I64\n", + "print(\"V100 I64/I64 UNIFORM\")\n", + "query = 'Key == \"I64\" and\\\n", + " Value == \"I64\"'\n", + "plot_bench(filter_bench(v100_dfs_mult, query), \"Multiplicity\", log_xscale=True, show_legend=False)\n", + "\n", + "###- A100\n", + "# I32/I32\n", + "print(\"A100 I32/I32 UNIFORM\")\n", + "query = 'Key == \"I32\" and\\\n", + " Value == \"I32\"'\n", + "plot_bench(filter_bench(a100_dfs_mult, query), \"Multiplicity\", log_xscale=True, show_legend=False)\n", + "\n", + "# I64/I64\n", + "print(\"A100 I64/I64 UNIFORM\")\n", + "query = 'Key == \"I64\" and\\\n", + " Value == \"I64\"'\n", + "plot_bench(filter_bench(a100_dfs_mult, query), \"Multiplicity\", log_xscale=True, show_legend=False)" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "V100 I32/I32 UNIFORM\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "V100 I64/I64 UNIFORM\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAX8AAAEeCAYAAABi7BWYAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAABM5klEQVR4nO3dd3gU1dfA8e9NIZCE0CKhdxKqBILSpXdQSmiiWH+oKCC+gl1ABRWwYAEVFWwkhAiKShepIk1a6CVU6S2EkJBy3z92E5IQyCTZ3dlkz+d55tnZmd2Zsxk4c+fOnXuV1hohhBCuxc3sAIQQQjieJH8hhHBBkvyFEMIFSfIXQggXJMlfCCFckCR/IYRwQfkm+SulvlVKnVVKRRn4bGWl1J9KqR1KqZVKqQqOiFEIIfKLfJP8gVlAF4OfnQJ8r7W+G3gLeNdeQQkhRH6Ub5K/1no1cDH9MqVUdaXUYqXUFqXUGqVULeuqOsAK6/xfwAMODFUIIZxevkn+t/EVMFxrHQK8CEyzLt8O9LHO9waKKqVKmRCfEEI4JQ+zA8gtpZQv0ByYq5RKXexlfX0R+Ewp9SiwGjgJJDs6RiGEcFb5NvljuWq5rLUOzrxCa/0f1pK/9STRV2t92aHRCSGEE8u31T5a6xggWinVD0BZNLDO+yulUn/bK8C3JoUphBBOKd8kf6VUGLAeCFJKnVBKPQEMBp5QSm0HdnHzxm4bYJ9Saj8QAEwwIWQhhHBaSrp0FkII15NvSv5CCCFsR5K/EEK4oHzR2sff319XqVLF7DBMd+3aNXx8fMwOQ6Qjx8Q5yXGx2LJly3mt9V1ZrcsXyb9KlSps3rzZ7DBMt3LlStq0aWN2GCIdOSbOSY6LhVLq6O3WSbWPEEK4IEn+QgjhgiT5CyGEC5LkL4QQLkiSvxAuLvODnvLgp2uQ5C+EC+v4fUdCI0LTEr7WmtCIUDp+39HkyIS9SfIXwkVprfHz8mPe3nn0jehLfGI8oRGhzNs7Dz8vP7kCKODyRTt/IYTtKaWY228uzb9tzvy98ykysQgAfWr1IbJ/JOnGyRAFkJT8hXBBWmuWHFxCk2+asOHkhgzrJPG7Bkn+QriYtcfW0ua7NnT5qQvnrp0jpGxIhvXtvm8nVT4uQJK/EC7i31P/0u2nbrSa2Yr9F/bzaddPCS4TzJZTW+hTqw9XXrpCYffCrDyykr4RfeUEUMBJ8heigNtzbg/95vYj5KsQNpzcwKQOkzg04hDP3fsc125cS6vj9yvsx6xeswDYfW63VP0UcHLDV4gCKvpSNONWjePHHT/i4+nD2NZjGdV0FMUKF0v7zLIhy9BapyX6/nX78/W/X7Ppv02ciT1DgG+AWeELO5OSvxAFzH9X/2PYH8MI+iyIiF0RvND0BQ6PPMy4NuMyJP5U6Uv4Sik+7/4515OuM3rZaEeGLRxMkr8QBcT5uPOMXjqa6p9UZ8a/M3iy0ZMcGnGIyZ0m4+/tb3g7gaUCGd18ND/s+IFVR1bZMWJhJkn+QuRzMQkxjFs5jmpTq/HhPx/Sv25/9j23j2ndp1GuaLlcbfPVVq9SuVhlhi0cRmJyoo0jFs5Akr8Q+VRcYhyT1k2i6tSqjF81ns41OhP1TBTf9fqOaiWq5Wnb3p7efNr1U3af283H/3xsm4CFU5HkL0Q+cyP5Bp9v/Jzqn1TnpeUv0aR8E7YM3cLcfnOpfVdtm+2nZ1BPegb2ZPyq8Ry/ctxm2xXOQZK/EPlEUkoSs7bNIuizIJ5b9Bw1S9ZkzWNrWDh4IY3KNrLLPqd2mUqKTmHUklF22b4wjyR/IZxcik5h7q651J9en8d+fYxSRUqxePBiVj26ipaVWtp131VLVOW1Vq/x856fWXJwiV33JRxLkr8QTkprzcIDCwn5KoT+kf1xU27M6z+PTf/bROcanR32ENaLzV8ksFQgzy16jvikeIfsU9ifJH8hnNCqI6toObMl3Wd3JyYhhh96/8COp3fQu3Zvhz956+XhxefdPufgxYNMWjfJofsW9iPJXwgnsunkJjr90Ik237Xh6OWjfNH9C/Y+u5eH7n4Idzd30+LqUK0D/ev2Z+KaiRy6eMi0OITtSPIXwglEnY2i95ze3Pv1vWw9vZUPOn3AgeEHeKrxU3i6e5odHgAfdvoQT3dPRiweIZ2+FQCS/IUw0cGLB3lo3kPcPf1uVkSv4O22b3N4xGFeaPYCRTyLmB1eBuX9yjO+zXgWHljIr/t+NTsckUeS/IUwwfErxxn621BqfVaLeXvm8VKLl4geGc3r971OUa+iZod3W8PvHU790vUZuXgk125cMzsckQeS/IVwoLPXzjJq8ShqflqTWdtmMeyeYRweeZh3O7xLySIlzQ4vW57unkzrPo1jV47xzup3zA5H5IF06SyEA1yOv8yUv6fw8T8fcz3pOo82eJQ3W79J5eKVzQ4tx1pWaskjDR7hg/UfMKTBEJs+VSwcR0r+QtjRtRvXeHfNu1SdWpUJaybQI7AHu4ft5psHvsmXiT/VpI6T8Cnkw7MLn5Wbv/mUJH8h7CAhKYFPNnxCtU+q8eqKV2lZqSVbn9pKeGg4Qf5BZoeXZ6V9SjOx3UT+OvIX4VHhZocjcuG2yV8plaKUSjYyOTJgIZxB5tJu6vuklCS+/vdran5ak5GLR1L3rrr8/fjf/DboN4LLBJsQqf0MDRlK43KNeWHpC1yJv2J2OCKH7lTn3x9I/RceALwFzAfWW5c1A3oBY+0VnBDOqOP3HfHz8iOyfyRgSfx9I/py4OIBEpISOHDxAE3KN2HmAzNpX629ydHaj7ubO9O7T+feGfcyduVYPu7ysdkhiRy4bfLXWkemziulFgCvaK1npPvIt0qpjVhOANPsFqEQTkRrjZ+XH/P2ziM0IpTnSj9Hi29bsP6EpUxUv3R9FgxcQI/AHi4xAHrjco15KuQpPt34KY8GP1rgrm4KMqN1/u2Av7JY/hfQxmbRCOHklFJE9o+kT60+zNs7j97rerP+xHp8PH2Y3Wc2257eRs+gni6R+FNNbD+RUkVKMeyPYaToFLPDEQYZTf7ngdAslocC52wXjhDOL/UEAHAl2VLXfemlSwyqPwg35XptKEoUKcGkjpNYf2I9s7bNMjscYZDRf6lvAhOUUkuUUuOs02LgHQzW+Sul3JVSbyulopVS8dbXd5RS8qyByFe01oRGWMpCxdyLATAwcqBLN3kc0mAILSu1ZMyyMVyIu2B2OMIAQ8lfa/090BzLFcD91ukC0EJr/Z3Bfb0EPAuMAGoBI63vX8lhzEKYJjXxz9s7D4CHqzycVgUUGhHqsicAN+XGtG7TuBx/mVf/fNXscIQBhq9RtdYbtNaDtdaNrNNgrfWGHOyrOfCb1vo3rfURrfUCYAHQJKdBC2EWpRQxCTHU8q+Fm3Kjbem2afcAYhJiXKquP7P6AfUZ2WQkM/6dwYYTOUkNwgw5qqBUSpVTSgUrpRqlnwx+fS3QVilVy7qtOlhuJC/MWchCmGvpw0tJTkmmTZU2lCxUMu0ewLIhy8wOzXTj2oyjbNGyDFs4jOQUeQTImRmqb1dKNQR+xFJdk7loowEjo0y8DxQFdlsfDPMAJmits2wmqpQaCgwFCAgIYOXKlUZCLdBiY2Pl7+AE9l/dz4GLB3jA/wE5Jll4osITvL3nbV4Ie4He5XubEoMcl+wpI3WUSqlNWOr43wL+4+bDXwBorY8a2MZAYDIwGtgFBANTgdFa62/u9N3GjRvrzZs3ZxtnQbdy5UratGljdhgub/TS0UzdMJXTL55mx4Ydckwy0VrT6cdObDq5iX3P7SPAN8DhMcj/FQul1BatdeOs1hmt9qkDjNBa/22trz+afjK4jcnAFK11uNZ6p9b6B+BD5IavyEdSdApzds2hc43O+aILZjMopfis62fEJcYxetlos8MRt2E0+e8EyuRxX95A5krA5BzEIITp/j7+N8djjjOw7kCzQ3FqQf5BjGkxhh92/MCqI6vMDkdkwWjifRWYpJTqoJQKUEqVTD8Z3MZvwMtKqe5KqSpKqd7AC1j6CxIiXwiPCqeIRxEeqPWA2aE4vVdbvUrlYpV5duGzJCYnmh2OyMRo8l8O3AssxVLnf846ncf4E77DgUgs/QDtAT4AZgCv5SBeIUyTlJLE3N1z6RHYA99CvmaH4/S8Pb35pOsn7Dq3i6kbppodjsjE6NO1bfO6I631VeB56yREvrMiegVnr51lUL1BZoeSb9wfdD89A3sybuU4BtYbSAW/CmaHJKwMJX+ttVTaCZcXHhWOn5cfXWt2NTuUfGVql6nUmVaHUUtGMbffXLPDEVaGb7Za6/rfUkpFKqXmWvv3cXwbLiFMkJCUwLw98+hdqzeFPQqbHU6+UrVEVV5v9TqRuyNZcnCJ2eEIK0PJXynVAjgIPAhcB+KBh4ADSqlm9gtPCOew+OBiriRckSqfXHqx+YsElgrkuUXPEZ8Ub3Y4AuMl/ylAGBCotX5Ya/0wEAiEY7lxK0SBFhYVhr+3P+2qtjM7lHzJy8OLz7p+xsGLB5m8brLZ4QiMJ/9g4AOtb47UYJ3/EGhoh7iEcBrXblzjt/2/0a9OPzzdPc0OJ9/qWL0j/ev2Z+LaiRy+dNjscFye0eR/BaiaxfKqwGWbRSOEE1qwbwFxiXEMrCcPduXVh50+xMPNgxGLRrhs99fOwmjyDwe+UUoNVkpVtU4PAV9jqQ4SosAK3xVO+aLlaVmppdmh5Hvl/cozvs14/jjwBwv2LTA7HJdmNPmPwfKA1rdYbvwexJL4I4CX7ROaEOa7dP0Siw4sYkDdAS45RKM9DL93OPVK12PE4hFcu3HN7HBcltGRvG5orUcCJbDU/wcDJbXWo7TWN+wXnhDmmr93PokpiQyqL618bMXT3ZPp3adz7MoxJqyZYHY4LstoU88ySqkKWus4a4+cO7XWcUqpCtLWXxRkYVFhVC9RnZCyIWaHUqC0rNSSRxo8wpS/p7D3/F6zw3FJRq9jfwSyeqyxM/CD7cIRwnmcjj3NiugVDKo3yKWHZ7SXSR0n4VPIh2cXPis3f01gNPk3BlZnsXyNdZ0QBU7k7khSdIq08rGT0j6lmdhuIiuiVxAeFW52OC7HaPL3ALyyWF74NsuFyPfCosKoX7o+dUvXNTuUAmtoyFAal2vM/y39P2ISYswOx6UYTf4bgGeyWP4ssMl24QjhHI5ePsrfx/+WUr+dubu5M63bNE7HnmbsX2PNDselGO3S+TVghVLqbmCFdVk7LE/3drBHYEKYac6uOQCS/B3gnvL38FTIU3yy8RMeDX6UBmUamB2SSzDa1PMfoBlwBOhjnaKBZlrrv+0WnRAmCY8Kp0n5JlQrUc3sUFzChPYTKFmkJMMWDiPlZi8ywo4MP7Witd6utR6sta5rnR7SWm+3Z3BCmGHf+X1sPb1VSv0OVLJISSZ3nMzfx/9m1rZZZofjEnLan/+LSqlpSil/67IWSqms+vwRIt8KjwpHoehft7/ZobiUIQ2G0KJiC15a/hIXr180O5wCz+hDXiHAPmAw8CTgZ13VEZBH9ESBobUmLCqM1lVaU65oObPDcSluyo1p3adx6folXv3zVbPDKfBy0p//VK11QyAh3fIlQAubRyWESbaf2c6+C/tk0BaT3B1wNyOajOCrLV+x8eRGs8Mp0Iwm/xDguyyWnwKkewdRYITtDMPDzYO+tfuaHYrLGtdmHGWLluWZP54hOSXZ7HAKLKPJ/zqWTt0yqwWctV04QphHa034rnA6Ve9EKe9SZofjsvy8/Piw04f8e+pfvtj8hdnhFFhGk/+vwFilVOrTvFopVQV4H/jZHoEJ4WjrT6zn2JVjDKwrrXzM1r9ufzpU68BrK17jTOwZs8MpkIwm/xeBksA5wBtYi6VP/8vA63aJTAgHC9sZRmGPwjxQ6wGzQ3F5Sik+6/oZcYlxjFk+xuxwCiSjD3nFaK1bAr2Al4CpQBetdWuttYzGIPK9pJQkInZH0L1md/y8/LL/grC7IP8gRjcfzffbv2f10az6lRR5kaOhibTWK7TWU7TWk4BVdopJCIdbeWQlZ6+dlVY+Tua1+16jcrHKDPtjGInJiWaHU6AYbec/QinVN937b4DrSql9Sqkgu0UnhIOER4VTtFBRutXsZnYoIh1vT28+6foJu87tYuqGqWaHU6AYLfmPwFLfj1LqPqA/8CCwDfjALpEJ4SAJSQn8vOdnetXqRRHPImaHIzK5P+h+egT2YNzKcZyIOWF2OAWG0eRfHktHbgA9gbla6whgHNDUDnEJ4TBLDy3lcvxlqfJxYp90+YRkncwLS14wO5QCw2jyjwFKW+c7An9a5xOxDOgiRL4VFhVGqSKl6FBNeid3VlVLVOW1Vq8xd/dclh5aanY4BYLR5L8UmKGU+hqoASyyLq/LzSsCIfKdazeu8eu+XwmtE4qnu6fZ4Yg7GN18NDVL1uTZhc8SnxRvdjj5ntHk/yywDrgLCNVap3a51wgIs0dgQjjC7/t/Jy4xTrpvzge8PLz4vNvnHLx4kMnrJpsdTr5naCQvrXUMMDyL5TLumsjXwneFU65oOVpVamV2KMKAjtU70q9OPyauncjguwfLYDt5kKN2/kIUJJfjL7PwwEL61+mPu5u72eEIgz7q/BEebh6MWDQCrbXZ4eRbkvyFy5q/Zz43km8wqL608slPyvuVZ1zrcfxx4A8W7Ftgdjj5liR/4bLCd4VTrUQ17il3j9mhiBwa0WQE9UrXY+TikVy7IT3M5IYkf+GSzl47y5+H/2Rg3YEopcwOR+SQp7sn07pN4+iVo0xYI4MJ5ka2yV8p5amUOq2UqpvXnSmlyiqlvlNKnVNKxSuldiulWud1u0LkVOTuSJJ1slT55GOtKrfikQaPMOXvKew9v9fscPKdbJO/1joRy8NcebqzopQqjqW5qAK6A7WxtCCSwWCEw4VFhVH3rrrUK13P7FBEHkzqOAmfQj48u/BZufmbQ0arfT4FXlFKGWoaehtjgFNa6yFa641a62it9Z9a6z152KYQOXb8ynHWHlsr3TkUAKV9SjOh3QRWRK9gzq45ZoeTrxhN/q2AB4CTSqk/lVIL0k8Gt9EL2KCUmqOUOquU2qaUek5JhatwsNQkMaDeAJMjEbbwVMhThJQN4YUlLxCTEGN2OPmGMnKppJSaeaf1WuvHDGwj9Xnsj4AIIBjLFcXLWuvPsvj8UGAoQEBAQEh4eHi2cRZ0sbGx+Pr6mh1GvvfUlqdwU25MbzQ9z9uSY+Ic9sbsZdjWYfQt35dnazwrx8Wqbdu2W7TWjbNaZyj524JS6gawWWvdPN2yiUBvrXXtO323cePGevPmzfYO0emtXLmSNm3amB1GvnbgwgECPwvkg04f8EKzvPcQKcfEeVT4sAInr55k69CtXN53mdatWxMaEUpMQgzLhiwzOzxTKKVum/xz1NRTKdVYKTVAKeVjfe+Tg/sAp4DdmZbtASrlJAYh8iI8KhyFYkBdqfIpSLTWNCzbEID237cnRacQGhHKvL3z8PPyk5vBWTA6kleAUuofYCMwGwiwrvoQ44O5rAMyj/oVCBw1+H0h8kRrTVhUGK0qt6K8X3mzwxE2pJRiwcAFNCrbiIvxFxm+ZTjz9s6jT60+RPaPlGc5smC05P8RcAYoBcSlWz4X6JSDbTRVSr2mlKqhlOqHZYSwz40GK0Re7Dy7kz3n90grnwJKKcXGJzcCsPuapZJBEv/tGU3+7YHXtNaXMi0/hMFqG631JiwtfvoDUcAE4A1gmsEYhMiTsJ1huCt3+tbum/2HRb6jtab/3P4ZloVGhEqVz20YTf5FgBtZLL8LMDyqgtb6D611A611Ya11oNb6Ey1HRjiA1prwXeF0rN6Ru3zuMjscYWNa67Q6/j61+tCtTDcUinl758kJ4DaMJv/VwKPp3mullDvwEjeHdBTCaW04uYEjl49IlU8BpZQiJiEmrY7/iapPUNSrKAE+AcQkxEjVTxaMttQZA6xSSt0DeGG5yVsXKAa0sFNsQthM2M4wvNy96FWrl9mhCDtZNmQZWmuUUpQsVJI373uTF5e9yMxmd3xMyWUZKvlrrXcD9YH1WMbzLYzlZm9DrfUh+4UnRN4lpyQTsTuC7oHd8fPyMzscYUfpS/jDmwynZsmajFoyihvJWdVauzbD7fy11qe11m9qrXtorbtprV/XWp+yZ3BC2MKqo6s4HXuagXVlnF5XUsi9EB91/oh9F/bx+UZpVJiZ4eRv7Y75LaVUpHV6SylVzp7BCWEL4VHh+BbypXtgd7NDEQ7WPbA7XWt0ZdyqcZy9Jh0Ip2f0Ia+OWJp1DsDSzj8OS5PNg0opo+38hXC4G8k3iNwdyQNBD+Dt6W12OMIEH3b+kLjEOF5f8brZoTgVoyX/T4CvgVrWLpmHaK1rATOAqXaLTog8WnZoGZfiL0krHxdWy78Ww+8dztf/fs3WU1vNDsdpGE3+VYDPsmiT/zlQ2aYRCWFDYVFhlChcgo7VO5odijDRm63fxN/bnxGLR0ibfyujyX8zltY+mdUH5FQqnFJcYhy/7vuV0DqhFHIvZHY4wkTFCxdnQrsJrD22lohdEWaH4xSMJv9pwEdKqZeVUm2s08tYOnb7TCnVKHWyX6hC5Mwf+/8g9kYsA+tJKx8Bjzd8nOAywYxeNpq4xLjsv1DAGU3+PwEVgInACus0EahoXbfZOm2yQ4xC5EpYVBhlfcvSunJrs0MRTsDdzZ1PunzC8ZjjTF432exwTGf0Cd+qdo1CCBu7En+FhQcW8nTjp3F3czc7HOEkWlVuxYC6A3h/3fs81vAxKhVz3eFEjD7he9ToZO+AhTDil72/kJCcIFU+4haTOk5CoxmzbIzZoZgqRyN5CZFfhO8Kp0rxKjQp38TsUISTqVSsEi+1eIk5u+aw5ugas8MxjSR/UeCcu3aOZYeWMbDuQOnNUWRpTIsxVPSryIjFI0hOSTY7HFNI8hcFzs97fiZZJzOovjzYJbLm7enN5I6T2XZ6G99u/dbscEwhyV8UOGFRYdT2r0390lk9miKERf+6/WlVqRWvrXiNy/GXzQ7H4Yz27eOmlHJL976MUupJpZT05S+cyomYE6w5uoZB9QZJlY+4I6UUU7tM5Xzced5a9ZbZ4Tic0ZL/H8BwAKWUL5Y2/ZOBlUqpIXaKTYgci9gVgUZLKx9hSMOyDXmy0ZN8uvFT9p7fa3Y4DmU0+TfG8mAXQB8gBigN/A940Q5xCZEr4VHhhJQNoWapmmaHIvKJd9q9g7enNy8secHsUBzKaPL3BS5b5zsB87XWiVhOCNXtEJcQOXbw4kE2/bdJSv05sO7YOuKT4jMs+/v43yQmJ5oUkeOV9inN2NZjWXRwEX/s/8PscBzGaPI/BrRQSvkAnYFl1uUlsfTtL4TpwqPCARhQd4DJkeQPJ2JO0HpWa+6beV+GE8C+8/sYGOlaJ9Dn7n2OoFJBLjXko9Hk/yHwA3ACOAmsti6/D9hph7iEyLHwqHBaVWpFxWIVzQ4lX6jgV4FRTUex6b9N3DfzPv458Q8rDq9gzPIxPHfvc2aH51CpQz4euHiATzd8anY4DmGobx+t9ZdKqc1AJWCZ1jrFuuoQ8Ia9ghPCqJ1ndrLr3C4+7yZjtWYnMTmRY1eOEX05msBSgbSv0p4/j/xJs2+a4ePpw2+DfqNt1bZmh+lwXWt2pVvNbry1+i0euvshAnwDzA7Jrox27IbWeguwJdMy16kgE04tPCocd+VOaJ1Qs0MxXYpO4b+r/xF9KZroy9GciT3D6BajAXjqt6f4euvXpKSV38DPyy9t3tPNk/0X9tO6SmvclOs9BvRR54+oO60ur614ja/v/9rscOzKcPJXSjUB2mNp5ZPhX4XWeoSN4xLCMK014bvCaV+tPaV9Spsdjt1prTkfd57oy9FpCf75ps9T2KMw76x+h7dXv52h3lqheO7e5yjiWYQ2VdoQ4BtA1eJVqVqiKttPb+f5Jc/j6ebJ/UH3s2DfAp7+42l+2vkTX/X8ilr+tUz8pY4XWCqQkU1G8uH6D3mm8TOElAsxOyS7MZT8lVIvApOAg8B/QPpx0GRMNGGqTf9t4vClw7xxn+NqILXWGR4iy/w+r67EXyH6cjRHLh8h+lI0g+8eTGmf0szcOpPhi4ZzLfFahs/3rd2XmqVqElI2hOebPE/VElXTEnylYpUo7FEYIEOXFydiTtDxh454unnyy8Bf6FazGysOr+D+8PvZeHIjDb5owOutXuflli/j6e5ps9/m7N647w1+2PEDIxePZM1jawrsw4JGS/4jgRFa68/sGYwQuRG2M4xC7oXoVauXQ/Y3buU4UnQK49uMT1u2/cx2Vh1ZxcimIw1t43ridUtityb4TtU7UaNkDZYeWsrAyIFcir+U4fN3B9xN+2rtqeVfiycbPZmW2KsWr0qV4lUo6lUUsNRbd63Z1VAMFfwq8Gjwo/Su1ZtuNbsB0K5aO95u+zbnrp3j8OXDRO6J5OWWLxvaXkFRrHAxJrabyJO/PUl4VHiB7SPKaPL3AxbaMxAhciM5JZk5u+bQrWY3ihcubvf9aa1ZeGAhm/6zDFrXTrUD4NL1S0Rfjk67Akh/UzX6UjSNyzWmYdmGRJ2NosP3HThz7UyG7c58YCY1StagcrHKDKw38JbkXrJISQCaVWxGs4rNbPZ7ZvScccuyUc1Gpc3HJMTg6e7JlfgrvL/ufV5p+UraiaYgezT4UaZtnsaY5WO4P+h+fAr5mB2SzRlN/mFAFyxj+QrhNNYcW8Op2FMMrOuYdulKKd5t/y49wnrw9uq3uVLxCp+c+YQV0SuYP2A+F65fIOSrEE7EnMhwU3V8m/E0LNuQMr5l6BHYgyrFq2RI8KktS4L8g5jW3Xn+m6XeDF58cDHvrX2PH3f8yPTu0+ke2N3kyOzL3c2dqV2m0mpmKyatm8T4tuOz/1I+YzT5HwfGWzty2wFkePxPa/2hrQMTwojwqHB8PH3oEdjDYftsX609vw36jY4/dOST458A0K1GN9pWbUuKTqFNlTZULlY5LblXKV6FCn4VAPD39s+XrUgG1BtApWKVePK3J+kR1oOB9QbyceePC3RzyJaVWjKw3kAm/T2Jxxs+TuXilc0OyaaMJv8ngViguXVKT2N5CEwIh0pMTiRyd6TDL8u11oxbOS7tffuq7fl5wM8AuCk3vuv1ncNicaRmFZux9amtvL/2fd5Z846llVVouNlh2dWkDpP4de+vjF42moh+EWaHY1NGH/KSAdyF01l+eDkXrl9gUD3H3ZDTWvPw/IdZd3wd7sqdQRUHsfjMYtYfX095v/LULFmzwLYOAcuTsG+0foN+dfvh7ekNwPErx7mRfIPqJQteN18Vi1Xk5ZYvM3blWFYdWUXrKq3NDslmcvwUh1LK19rHjxCmCosKo3jh4nSq3smh+01ISqCQeyEWPriQJ6o+QURoBH0j+jJp3aQCnfjTq+Vfi0rFKgHwwtIXqD+9PpPWTSIpJcnkyGzvxeYvUqlYJUYuHlmghnw0nPyVUs8qpY4BV4AYpdRRpdQw+4UmxO1dT7zO/L3z6Vu7L14eXg7Z56IDi1hzbA2NyzVm0YOL6FTDctJpW7Utkf0iCSwV6JA4nM3HnT+mc43OvLT8Je6dcS//nvrX7JBsytvTmykdp7D9zHa+/jf/3a+5HaMjeb0KvAd8g6VL507ATOA9pZRrNQIWTmHhgYXE3oh1WJXP6qOr6RPRh9dWvMaYFmNoV61dhvXtqrVjTIsxDonF2ZT3K8/8AfP5uf/PnIo9xT0z7mHennlmh2VToXVCua/yfby24jUuXb+U/RfyAaMl/6eBoVrr8VrrP63TOOAZ65RjSqlXlFJaKSUPjokcC4sKI8AngDZV2th9X/+e+peeYT2pUrwK8wfMd5mqnZzqU7sPe57dw6imo2hftT0A125cy+Zb+UPqkI+X4i8VmCEfjSb/0sCmLJZvBHLc1ksp1RQYiqXZqBA5EpMQwx8H/qB/3f64u7nbdV97z++l84+dKVG4BMseXoa/t79d95ffFS9cnCmdplCscDESkxNp/m1zHvnlES7EXTA7tDwLLhPM/xr9j882fcaec3vMDifPjCb//cCDWSx/ENiXkx0qpYoBPwGPAwXj+kk41K97fyU+Kd4hI3Z9uflL3JU7yx5eltZWXxiTolPoGdiT2TtnU/vz2szeORut83dXYG+3fRsfTx+eX/J8vv8tRpP/OOBNpdRypdR467QceB0Ym8N9fgVEaq3/yuH3hAAgfFc4lYtVplkF23VzcDtTOk1hw5MbZEzgXPDy8OKddu+wZegWqhSvwuB5g+k+u3u+vgq4y+cuxrUZx9JDS/njQP7u0V4ZPXsppUKAUUBt66I9wAda662Gd6bU/7DcP2iqtU5USq0EorTWtwwbpJQaiqVqiICAgJDw8IL9MIkRsbGx+Pr6mh2Gqa4kXqHv+r70q9CPp6o9ZZd9xCbFMmX/FJ6p9gwBhe9cqynHxJhkncz8k/NZeW4lHzf4GA83w73J54o9j0tSShJPbHmCZJ3Mt42/pZBbIbvsxxbatm27RWvdOMuVWmuHTEAQcA4ISrdsJfBZdt8NCQnRQuu//vrL7BBM9+XmLzXj0FtPbbXL9mMTYnXzb5prz7c89bJDy7L9vByTnElJSdFaa33p+iX9QNgDesfpHXbZj72Py+IDizXj0JPWTrLrfvIK2Kxvk1dvW+2jlCqZfv5Ok8GTUDPAH9illEpSSiUBrYFh1veOaawt8rWwqDCCSgXRIKCBzbd9I/kGfSP68s+Jf5jddzYdqnWw+T5cXWpLqX3n9/H38b9p9FUjXl/xeoYB5PODzjU60yPQ0rnf6djTZoeTK3eq8z+nlEodFuk8llJ75il1uRG/APWB4HTTZiDcOn8jy28JYfXf1f9YdWQVg+oNsnlzy+SUZB6a9xBLDi3hqx5fyXCQdtakQhP2PLuHwfUHM2HNBBp80YDVR1ebHVaOfNjpQ+KT4nn1z1fNDiVX7pT82wEX081nNbW1vmZLa31Zax2VfgKuARet7/P3rXNhdxG7ItBou7TyiUmI4cDFA0zpOIUnGj1h8+2LW5XyLsWsXrNY+tBSEpMTeX/d+2aHlCM1S9Xk+abPM3PbTDadzKolvHO77V0XrfWqdPMrHRKNEHcQFhVGwzINCfIPstk2tdak6BRKFCnB+ifWpw13KBynY/WORA2LIvZGLADRl6L599S/9Kndx+kfqHv9vtf5bvt3jFw8knWPr3P6eNMz2r1DcroqoPTLSymlct3Tkda6jc6ipY8QmR2+dJiNJzfavDuHd9e+S685vUhISpDEbyJvT29K+1hSzEf/fETo3FB6z+nNyZiTJkd2Z35efrzb/l3Wn1hPWFSY2eHkiNF2/rc7nXkhdfXCAcKjLE19B9QbYLNtTt80nddWvEbxwsVdaoByZ/dBpw94v8P7LDm0hDrT6jB90/QMo6I5m0eDHyWkbAhjlo3JV91Z3DH5K6VeUEq9gGXAlqdT31un0cAXwF5HBCpcW3hUOC0qtkjrRjivZu+czbMLn6VnYE++vf9b3FSOezcXduLp7smYFmPY+cxOGpdrzLCFw/jg7w/MDuu23JQbn3T9hJNXT/Le2vfMDsew7J60GG59VVhG80pfxXMDOILloS0h7GbX2V3sPLuTT7t+apPt/b7/d4bMH0LrKq2J6BchpX4nVaNkDZY/vJyfdv5Ez8CeABy9fJSyRctSyN25HqxqXrE5D9Z/kMl/T+aJRk9QpXgVs0PK1h2LO1rrqtoyitcqoEHqe+sUpLXurLXe4JhQhasKjwrHTbnRr04/m2yvrG9ZOlXvxIKBC6Se38kppXjo7ocoVrgYySnJ9AjrQaMvG7H++HqzQ7vF+x3ex93NnReXvmh2KIYYutbVWrfVWksnbMLhtNaERYXRrmq7PA8Wfib2DAAh5UJYOHghRb2K2iJE4SDubu683+F9YhJiaPFtC4YvHM7VhKtmh5Wmgl8FXmn5Cj/v+Zm/op2/67KcjOQVqJR6VSn1hVLq2/STPQMUrm3LqS0cunQoz618dp/bTd1pdflo/Uc2ikyYoVvNbuwatovh9w7n802fU2daHfZf2A+Q5RCLjh528f+a/R+Vi1Xm+SXPO/2QlkabenbH0vd+TyxdMQcB3YDeWLpsEMIuwnaG4enmSe9avXO9jSOXj9Dph054untyf9D9NoxOmKGoV1Gmdp3K30/8TatKrahavCobT24kYEoAM7fNzPDZAXMH8PJyxw02WMSzCFM6TWHHmR3M2DLDYfvNDaMl/7eA8VrrZkAC8DBQBViOpXM2IWwuRacwZ9ccutbsSokiJXK1jdOxp+nwfQfiEuNY+tBSqpesbuMohVmaVmjK7L6z8XT3RGvNpfhLPPHrE8zcajkB/BX9F6uOrqJJ+SYOjatv7b60qdKGN/56g4vXL2b/BZMYTf5BwBzrfCLgrbWOx3JSeN4OcQnB2mNrOXn1JAPr5q47h8TkRLr82IXTsadZOHgh9QPq2zhC4SzKFi1LwzIN0WgeX/A4b2x/g35z+xHRL4LetXN/1ZgbSik+7vwxl+IvMX7leIfuOyeMJv+rQGqziFNADeu8B5C7IpkQ2QjbGYa3p3euq2o83T0Z3Xw0vwz8haYVmto4OuFMKhWrxMb/beTBepYBB9deXku7qu1oW7WtKfE0KNOAoY2G8vmmz9l1dpcpMWTHaPLfALS0zv8BfKCUGgvMBJyvzZXI9xKTE4ncE8n9QffjU8gnR99NSEpg48mNAAy+e7B0zewiVh9dTeTuSAA8lAcL9i0wtdXN2+3epqhXUUYtGeWUQz4aTf4vAP9Y58cBS4G+wEEsD38JYVN/Rv/J+bjzOa7ySUpJ4sF5D3LfzPs4fuW4naITzmb10dV0/qEzyTqZn/v/zJeNvmT+gPn0m9uPH7b/YEpM/t7+jG8znmWHl/Hb/t9MieFOsk3+SikPoBZwEkBrHae1fkZrfbfWOlRrfczeQQrXEx4VTjGvYnSp0cXwd1J0Cv/77X/M2zOP9zu8T8ViFe0YoXAmlYpVolqJakT0i6BP7T5U861G15pdaVulLdM3TycuMc6UuJ5p/Ay1/WvzwpIXSEhKMCWG28k2+Wutk4B5gDwRIxwiPime+Xvn06d2H7w8jA3wprXm/5b8H7O2zWJs67GMbDrSzlEKZ1KleBX2PLeHPrX7ZFj+WMPH+OfEPwyIHEBicqLD4/J09+TjLh9z6NIhPv7nY4fv/06MVvts5+ZNXiHsatGBRcQkxOTowa4F+xbw8YaPGXHvCMa2HmvH6ER+0q1mN6Z1n8bv+3/n8QWPm9I7aKfqnbg/6H7eWfMOp66ecvj+b8do8h+H5SZvL6VUxVyO4SuEIWFRYZT2KZ2jlho9g3oyu89sPuryUb4aUEPY39ONn+adtu/w444fGbXYnJuvH3T6gISkBF5d4TxDPhpN/n9gGX93HpaePHMzhq8Q2bqacJXf9/9Ovzr98HDLrtNZiNwdyeFLh3FTbgyqP0i6ZhZZerXVq4xqOop5e+dxPu68w/dfo2QNRjUdxaxts9JaopnN6P+UtummXI3hK4QRC/Yt4HrSdUPj9P6691cGRg7kjb/ecEBkIj9TSjGl0xS2DN3CXT53mRLD6/e9ThnfMoxYNMIpBqcx2qvnqjtN9g5SuI7wXeFU9KtI84rN7/i5FdEr6B/Zn5ByIXzR/QsHRSfyMzflRmmf0qToFEYsGkHYTscOu1jUqyjvtn+XDSc3MHvnbIfuOys56dWzvlLqM6XUIqVUWeuyXkqphvYLT7iSi9cvsuTgEgbUHXDH6puNJzdyf9j91CxZk0WDF0nXzCJHbiTfYMeZHQz5ZQiLDixy6L6HNBjCPeXu4aXlL6UNWG8Wo716dgI2AeWxVPMUsa6qDkjTCmET8/bMIzElkUH179zKZ9zKcQT4BrD04aWULCLtDUTOFPYozK8Df6V+6fr0jejLumPrHLZvN+XG1C5T+e/qf7y75l2H7TfLWAx+7m3gBa11bzIO2L4SuNfWQQnXFBYVRmCpQBqWufPF5JzQOawYsoJyRcs5KDJR0BQrXIzFDy2mgl8FeoT1YMeZHQ7bd7OKzXjo7of4YP0HHL502GH7zcxo8q8HLMxi+UVAil4iz05dPcVf0X8xsO7ALJtqnrp6iicXPEnsjViKehWlcvHKJkQpCpLSPqVZ9vAy/L39ORFzwqH7fq/9e9kO+ThsGHh4gFKW12HDbBuD0eR/EUuVT2aNAMf+1USBNHf3XDQ6y1Y+F69fpNOPnZizaw6HLh4yITpRUFUuXpndw3bTrWY3AIeNvlXerzyvtnyV+Xvn8+fhP29ZP2wYTJ8OydaByJKTLe9teQIwmvxnA5OVUhUADXgopVoDU4DvbReOcFVhUWE0CGhA7btqZ1geeyOWbj9148CFAywYuIAGZRqYFKEoqDzdPQH4fvv33DvjXi5dd8xw5f/X/P+oUrwKzy95nhtJSVy9Cv/9B/v3w5dfZv2dr76y3f6zf4rG4nVgFnAUUMBu6+tsYILtwhGuKPpSNP+c+If32r+XYXl8Ujy9wnux+b/NzBswz7S+2UX+MWyYJUEmJ7fG3R2GDoVp04x9t1zRcuw6t4seYT1Y9vAyvD29s/yc1hAXB7GxcPWq5fVO87dfV5iLd33AkS598Wr+FWzKvlifbMMhiQ0lf611IjBYKfUm0BDLFcNWrfUB24UiXNWcXZZB4gbUG5Bh+YmYE+w+t5tZvWbJ2Lt2dDNhkuOE6UxSq0osVFpVSVISTJhgJFl3oEPCbBYl9af6K6EE7/2FuKuFbvnctWuWE4AR7u5QtKhl8vW1TEWLQqlSlnkf394sTmrL+W5v8HK/gQT4lcTXF4YMgZQsngNzd7fVX8t4yR8ArfUhpdQZ67y5jVRFgREeFU6zCs2oUrwKQFrfKzVK1mD/8P34FvI1MbqCLWPCvFm3fOECvPKK5X1S0s3pTu/N+Gz6+e3bs/6NM2ZYpuy4uYGvb1+KNf6C0/cNZUPZR6kf+yPly7tlSNyp85nfZzVfqJDlhu3tKXaemUrwl8GcqzuWN7t9CsC6dRmPS6qhQ7P/HUYZTv5KqeexDOpS3vr+P+BD4GPtjMPUiHxhz7k9bD+znaldpgKWxD9y8UgKuRdicsfJkvizkZJiKY1euQKXL1te089n93r6dNbbjYiwTPbi4WGZ3N1vzmf3PvO6IkUyrrtd8gf49NPsE3fhwqmJ+n+8v/YiCckJvHGfyiZ55139gPo8HfI00zdP56nGT1GvdL20Ky97XpEpI3lbKTUJGApM5uawjc2AF4EZWusxtgvpVo0bN9abN2+25y7yhZUrV9KmTRuzw8iTzFUMwaPGsrXoO5x84SRlfMsw9q+xvLX6LUY1HcUHnT5w2h46b/4Ojbu7yvV/zPh4Y0n6dutiYrKvgihcGIoVg+LFb329U4l43jzbJOnM793s1Peeh0fWdeLu7pYrg9w6d+2c3fsDuhB3gZqf1qRR2UYse3iZzf7dK6W2aK0bZ7XOaMn/SeBJrXVkumUrlFL7gC8BuyZ/UTDcWsWg2ZIQRnnPNpTxLcPH/3zMW6vf4rHgx5wy8WttKWmnJn6Lm3XL//0Hjz9uvNR95QokZDO4k5ubJVGnTsWLQ5UqWSfy9K/p573uMB7Ot9/ePmH27p2Tv475hg61fVXJgQsHaPpNU8a2HsuIJiNyv6FslPIuxVtt32L4ouH8uu9XetXqZbd9pTJa8r8INNVa78+0PBDYoLUuYaf4ACn526qUaW8pKZaSbFycZbp27eZ8XBx065bpJlbZf+GpEFgwg7593Pk56XHq0JfeyeHoZI+0+tzk5JtT+vc5nbfF93PK29t4ks7q1dc3uzrjvMl8Qk71zDPO+W8sO7b+v5KUkkS/uf34Ze8v/ND7Bx66+yHbBZvFvoK/COZ60nV2DdtFYY/Ced7mnUr+RpP/x9bPjsy0/CPAXWttv1Mirp38bfWfMzk5YyK+XYLOy/rr13P44zqOhqZTYcppqLoCGnwHEZGQ7IW7+83qgpzMO+I7Y+/Qm9XmzRkTuKdnDv8mJigorX3Ss2UVaXxSPN1nd2fVkVX8OvBXugd2t8l2s7L88HI6/tCRie0m8kqrV/K8PVsk/+nAg8Ap4B/r4iZAOeAnIK1GzR4nAldK/snJlnrc1OqBkJCsm3wpBc8+azxB37hx6zay4+lpKbl6e4OPz835203ZfaZVq3S/RaXA85XhbF3cwxdz9Sq4uWk8PBRubvYt7eaVveqWhe3Y+v7Y1YSrtPu+HVFno1j3+DoalW1ks21n1iu8F8sPL2f/8P157r/KFnX+tYB/rfOpnaqctk7pH8l0mlY/ZpRmtLZUexit883q9epV4/uaPfvWhOvrC6VL5ywpZ7W+SBHbllpPx56m5cNRrP6ug2VBxb+h2AkocpFOz/1GkSI9sTw36PzsUbcsnFtRr6IsfHAhE9dMpM5ddey6rw86fUCdaXV4ZfkrfNf7u7TlWmub3gcz+pBXvnq08nZtl+HOJ4DUUndekndi4p1jc3e/tW63Zs3b1/mGht7+YY8LFwz9OZzCZxs/Y3XVCdT633gOfPsmyfd8ChoKuXlTu+tKoKfZIRqWsRmec9+HEbZzl89dfNTlI8DS39Tl+MtUK1HN5vupXrI6ZX3L8v2O73mm8TM0rdgUrTWhEaHEJMSwbMgym+wnJ+38iwE1rW8Paq0v2yQCO7hd/xdffGGpTrhd8jZS6vbxyZig77rrzsk786u3d86qNJ56qmCUMse3Gc/GkxtZxlhaf72C1UdX4+lWCN8iKfQI7GF2eDk2bZplWrlyVb5vfityLjQilOjL0ax9bC3l/bLq8zL3tNY0CGjA0StH6Ta7G+dGn6P/3P7M2zuPPrX62OwKINvkr5SqBHwOdOXmdblWSi0EhmutjxrZkVLqFaAPEAQkYLl38IrWOio3gd9JctP34eS9cCTdBUuVv9DlNzJnzkuGS92ZX/38HH8Dr6CUMpVSpOgU/Dz9WHXUMvKnp7snde+qy8Q1E6XfHpGvTOo4ibbftaXzj51Z/dhqmw4qpJTil4G/cO+Me9l8ajMeb1vSdJ9afYjsH2mzqp87Jn+lVHksSToFeBNLh24AdYFhwN9KqXu01v8Z2FcbYBqWEcEU8BawXClVR2t9MXfh30prDRdqQb/+8NuXcLohFI+GfgPgt685d8629WaOkB9LmVprdpzZwaKDi1h8cDFe7l54unkSkxiT9plyRcux5vgam5ZmhHCExuUa8+vAX+n6U1e6z+7O8oeX41PIx2bbV0rxz5P/pCV+wKaJH7Lv0nksEA3U1FpP1Fr/Yp0mYKkCisbgMI5a685a65la6yit9U7gYeAuoEUe4r+FUoqn29wPP/8EfYbA89XgkQ5wtSz1ev7JrG2z+PfUvyQkZfN0jci1D/7+gPIflif4y2Be+fMVTl49SUJyAmuPr83wuQMXD9CyYkub/6MWwhHaVW3HnNA5bDy5kRGLbNvIUWtN/7n9MywLjQjFSOtMo7Kr9ukGDNZa39KCW2sdp5R6Hfgxl/suiuXkY/POs6dPV/BMR77Y3QeCf4Az9Qgo6cthn294fEEcAB5uHtT2r01wmeC0qUFAA0p5l7J1OAVWik5h66mtLDq4iKWHljK1y1QOXzrMn9F/opSiVJFSXLh+gYMXD3Lw4kHA8ndPP2DG5lObHfZEoxC21qtWLyJCI2haoanNtpl6cze1jj+yf2Ta+9CIUJsVlu7Yzl8plQBU11pnOVqXdXCXQ1rrOzxAftttR2C5emistb6l1bRSaiiW/oQICAgICQ8PN7ztI5ePEBUTxRfHvqBZsWasv7Kepys9TYvSLYhNieVg7EEOxh7k0LVDHIo9xPkb59O+W9qrNNV9qlPDtwbVfS2vZQuXxU3ZqUOSHIiNjcXX1/yOzvZd3ce30d8SFRNFXLLlZKpQaGtLXzfcqOhdkeo+1anuWz3t77no1CLiEuNYcGZB2nHpGdATPy8/BlW686DtzspZjonIyIzjkqyTWXF2BR1Kd8hzcv7q4FdULlKZzuU7py1bcnIJR68fZWgN46092rZtm7uHvJRSx4GHtdYrb7O+LfC91rqi4Wgs3/sQGAi01FpnO4JxTh7yuppwlYApAVxPuk6T8k1Y/8T6tLNmtRLVODj84C0H5uy1s2w/vZ1tp7ex7cw2tp3ext7ze0nRljaWRQsVpUGZBgQHBFteywRTr3Q9mzx+nROO7thNa83Ry0f5ec/P/HHgD5JTkjl97TT7L9zs5aOwR2EaBDTgnnL3pP1t6t5VlyKeRW7Z1u1KM7a+keVIBaGzvYLIjOMye+dsBs8bzGutXuOddu/keXuZ74Pl5r5YXh7yWgS8o5Rqr7XOUEmulCoMvE3WA7vfKZiPsCT+tkYSf04V9SpKYKlAdpzZwYaTGyj+fnHea/8evx/4neJexbP845X2KU3H6h3pWL1j2rLridfZdW6X5YRgnWZtn0XsJsswBu7KnVr+tW6pNrJ373/2kpCUwO5zu9l+ZjvbTm1j6eGlHLp0iBvJNx8NLlG4BG2qtGFQ3UHUD6hPSLkQKherbOgfpFKKmISYDIk+9QQQkxCTLxO/EOkNqjeIlUdWMmHNBEoVKcWoZqPytL3M/yds/X8ku5J/OWAzkAx8Buy1rqqDpbWPB5Zqm5OGdqbUVGAAlsS/x2iQueneYf6e+fSJ6JP2PsAngAZlGhBYMpCQciE8GvxojrYHljruw5cO33KVcCLmZq1YuaLlLCeDgJsnheolq9uk2shWpZnUK53tZ7anndj2nt9LsrX2rYhHEVJ0Cm7KjfoB9elWoxuPBD+SNthKXtiiNONMpOTvnMw6LskpyQz8eSCRuyOZ9cAsHgl+xOExpJfrkr/W+j+lVHMsTTQnkq6dP7AEeC4Hif9zLC18egGXlFJlrKtibT0q2I3kG8zaPivtfYOABtS5qw77L+xn1vFZbDuzLS35N/+mOdcSrxFYKpCgUkEElQqiYdmG1Ctd75btuik3apSsQY2SNehbp2/a8vNx59NOCKkJdcnBJWnJ1MfThwZlGtAgoEHaCaFe6Xq3HSM0s9wmzKSUJPZf2G+Jy5rst5/ZzunYmyN4FPYoTFJKEsk6mWJexVj/xHoCSwVy5toZyviWsfm9DnuXZoQwk7ubOz/2/pHL8ZcZsXgEPQJ7OG0jEkMduwEopUqQ8QnfHLXNV0rdbkfjtdbj7vTdnJT8byTfoN/cfizYtwDfQr483+R5vtjyBRGhEVQtUZXKxSoTeyOWol5FAXh9xetsO72N/Rf2c/jSYZJ1MgPrDSSsbxgA3Wd3p4xPGYL8g9JOENVLVqeQe6E7xhGfFM/uc7szVBttP7OdmARLO3c35UZQqaAMVUbBZYIJ8A3IsJ2O33fEz8uPyP6RrFq1itatW2f5mPfl+Ms3E7z1dde5XcQnxQPgoTyoXLwyzSo2o1GZRqw7vo6f9/xMuaLl6FK9C11rdqVDtQ4UL1zc0N85Vzw9s+75zMMj+34xnElB+R0gv8VOYm/Esu/8PkLKheRuAzb6LXnu1dNsOUn+1xOv02pmK/Zd2MeCgQtoW7Utf0X/Ra85vXj2nmeZ2H7ibb97I/kG0ZeiAQjyD+J64nU6/9iZfRf2cfba2bTPvdjsRSZ3mkxcYhyvr3idoFLWE4N/EGV9y962NKu15sjlIzdPCNZqo2NXjqV9poxvmbRqowZlGjBz60yWHl5Kn1p9GB4wnE/OfML8vfNpUr4JHat1ZMfZHWw/vZ2jV24+aO3v7U+DgAZULV6VZJ3MkctH2PzfZq7euMrqR1fTqnIrDl86TOyNWOqXru+40ne9erBr163L69aFKJs/6G0/BeV3gPwWB5ix5StCyoZYegJNn29vNw9w992wJ4ua8Rz+FpdK/gDvr32fe8vfm6HLgD8P/8mWU1sY0yJ3g45djr/M/gv72X9hP7X8a9G4XGP2nd9Hwy8bcj3p5mMQvoV8+bLHlzxY/0HOXTvHssPL0k4OqVcbmV28fpEdZ3ZkuErYdW5XWnt4N+VGik6hmHsxriRfSfuem3IjsFRg2lVDHf861ChVg9r+tdlwcgPNvmkGQKVilehaoytdanShQ7UO9hsXV2u4eNEyMOypU5Yp/fzBg5YO7zMrXNh+Y/vZQ+qoNZmlDptlZNCAnCzL7feMbOvECXj99Vt/yzvvQLlytx8JJyfLHPW9GzcsPTNm5uV1s0Mto8nXyOfu9H2r2EJQbxjEecLabyEwr50xbt9uOTEY5HLJ35GXfyk6hZMxJ9l3YR/7L+xn3/l9PNzgYRqXa8zv+3+nZ9jN3irL+pYlyD+Ijzp/RHCZYC7EXeBS/CWqFK+Ch1vG2y8JSQnM3jmbJxc8SXCZYP49/W/aurZV2uLn5cfsvrM5HXuaxQcXs+jgIlZEr2BY42FM7jSZxOREPt/0OZ2rd6aWf628le4TE+HMmVsTeuYkf/p01gMHeHtD2bKWafv2jD3olSoFjz2W+9jMMnNmxm5VS5aEgQOdIxGawc3NOU54s2dbCiCpSpWyjK2Z/t+/kXmjnzPw/f1coKX6liJ4so7HqaCKG/v+p5/C2Zs1Drm5gnG95O8kl38JSQkcuHgg7aSw/6Ll9dsHvqWWfy2+2PwFz/zxDJ5unlQvWZ3AUoEElgzkpZYv4e/tbxlA4rt2bD5l+e0dSnZg+cXlFHIvxO+DfufNlW/yzwnL2DrVSlSja42u9K/bn/sq32cswKtXs07imefPn8/6+/7+loRepszN5J7VfNF0VzzbtkHDhjff57Ak4zSc+XekpOTspBEVBX1utozjl18s/4eMJl5nGn3HSY/Lv6f+pc2sNlTwq8Dqx1bj7+2f/Zds8FtcL/ln/qOlqlkTSpS4OVpJ6mv6+dwsK1LE8h8hhw5fOszqo6sznBgOXjzImRfPUKxwMV7981WmrniXeHdwBypdhugS4JsAM36Hw1+8i7enN11rdKVGyRqW0n1KCpw7l31CP33aMsRXZp6e2SfzsmUtI8YUuvNN79tKPTnnx3rl9OrVQ+/ejapTJ3//Dig4xwSc9risOrKKLj914aseX/Fwg4eNfSmPx8X1kj/cWvovVgzatbs52Gzqa/r5uLjcVwuljnmY+SSRwxNLcuFCuPsUhSJFWHj1X15Z9CLuickcKgkxheHeEzB+BawMKsR7IWNuTexnzmQ9xqCfn7FSesmS9i/FbdsGzZrBhg1OUSrLtW3buBwaSvF58/L374CCc0zAqY/L8SvHqVgsBx0i5PG4uGbyz+0lU1LSzZNC5hPDnU4aufl8grGeRf+qAv37wTObYXpjiJgLbY9gudwuXTrrJJ55mbexZwpEzshDXs7J2Y/LmqNrmLltJjN6zsDdzd1u+7HFGL75T3Cw5VIp9ZLJ6FnTw8NSR52+ntpekpMtrUZuc7KIit7I8LWvsq0s/BwO7Y5A22joN9CNsfe9yfCOr+WqukkIYa6tp7cyc9tM3JU7X/X8ypSHHQt25vjxR8sl0+zZZkeSNXd3y7iQPlkPAhGUfB+FvFYy+66etHtvOGAp8T9V+yGu+npK4hcinxrRZARnr51lwpoJ+Hv7826Hdx0eQ8HOHsHBllJ0PuXp7smSh5dY3tT9Iu0m1oQh35kbmBAiz95u+zYX4i7w3rr3KOVdihebv+jQ/Rfs5F+Q/PgjV0JDKe6sVzFCiBxRSvFZt8+4FH+JLae2OLyTQ0n++UVwMNu+/po2TtZ6QQiRe+5u7nzf+3vclTtKqbTedB0hHz1TL4QQBU8h90K4u7lzIuYEjb5sxIroFQ7ZryR/IYRwAt6e3iTrZB4If4BVR1ZlWJeiU7h2I4uHMvNAkr8QQjiBkkVKsuShJXgoD9p+15ZPN3yati4mIYZ+Ef2YsHqCzfYnyV8IIZxEuaLlmNplKhrNiMUj+HLLlySlJLH11FY2/reRZhWa2WxfcsNXCCGcyJDgIcQnxfPUH0/x9O9PM3HNROIS45jbb26GburzSkr+QgjhZJ4MeZKaJS0DJx67coynQp6yaeIHSf5CCOFUUnQKzy18jgMXD+Dh5kHf2n35csuX/BX9l033I8lfCCGcyJhlY5i+eTpFPIqwZPASIvtHEhEaQe85vVl8YLHN9iPJXwghnEhonVA6VO3A74N+p121dgAElwlmcP3B7Di7w2b7kRu+QgjhRJpWaMqyIcsyLCtRpASfd//cpvuRkr8QQrggSf5CCOGCJPkLIYQLkuQvhBAuSJK/EEK4oHwxgLtS6hxw1Ow4nIA/cN7sIEQGckyckxwXi8pa67uyWpEvkr+wUEpt1lo3NjsOcZMcE+ckxyV7Uu0jhBAuSJK/EEK4IEn++ctXZgcgbiHHxDnJccmG1PkLIYQLkpK/EEK4IEn+QgjhgiT5CyGEC5LkXwAopeYrpS4ppSLNjkVYKKWKK6U2K6W2KaWilFL/MzsmAUqpI0qpHdbjYtuhsfIZueFbACil2gBFgUe01qHmRiMAlFLugJfWOk4p5QNEAY211hdMDs2lKaWOAPW01rFmx2I2KfkXAFrrlcBVs+MQN2mtk7XWcda3XoCyTkI4BUn+JlNK3aeUWqCUOqmU0kqpR7P4zDClVLRSKl4ptUUp1cqEUF2KLY6LtepnO3ACmKy1lr5m8sBG/1c0sEoptUkpNdghgTspSf7m88VSJTASuJ55pVJqADAVmAg0BP4GFimlKjkySBeU5+Oitb6stW4AVAUeVEoFOCLwAswW/1daaq1DgPuBV5VSd9s9aicldf5ORCkVCzyntZ6VbtkGYIfW+n/plh0AIrXWr6Rb1sb6Xanzt7G8HJd066YBK7TWclPeBmx0TCYDu9Jvw5VIyd+JKaUKASHA0kyrlgLNHR+RAGPHRSkVoJQqap0vBtwH7HNknK7E4DHxSXdMfIF2wC5HxulMPMwOQNyRP+AOnMm0/AzQIfWNUmo50ADwUUqdAPpprdc7LErXY+S4VAa+Ukql3uj9VGu903EhuhwjxyQAmG85JLgDM7TWmxwWoZOR5F8AaK07ZP8p4Uha641AsNlxiJu01oexFJIEUu3j7M4DyVhKLOkFAKcdH46wkuPifOSY5JAkfyemtb4BbAE6ZlrVEUtLBmECOS7OR45Jzkm1j8msN55qWN+6AZWUUsHARa31MeBD4Ael1EZgHfA0UA74woRwXYYcF+cjx8TGtNYymTgBbbA8eJJ5mpXuM8OAI0ACltLNfWbHXdAnOS7ON8kxse0k7fyFEMIFSZ2/EEK4IEn+QgjhgiT5CyGEC5LkL4QQLkiSvxBCuCBJ/kII4YIk+QshhAuS5C9cllJqnFIqysDntFLK8DgJSqkq1u80zuq9reISIi8k+Yt8Qyk1y5pEv8li3fvWdb/ncftZfb8s8Ftutwsct25jm8HPTwFaG4hLiFyT5C/ym+NAf6WUT+oCpZQHMAQ4Zo8daq1Pa60T8vD9ZOs2kgx+PlZrfSG3+xPCCEn+Ir/ZARwA+qdb1h2IB1amLsiqtHyn6hSl1DjgEaC79QpCW4fGzFDtk64K50Gl1FrrQOF7lVKdbhdwVtU+Sqla1sHIryilYpVS65VS9TPHebu4lFIrlFKfZdqPn1IqTinV505/QCFAkr/In74BHk/3/nFgJpZOvnJrChABLMdSRVOWO3cFPAn4BMuALcuAX5VS5Y3sSClVDlhrjbcj0Aj4HMvoUkbjmoFlUHivdJ8dBMSStyoq4SIk+Yv8aDbQWClVUylVBugCzMrLBrXWscB1IMFaRXNaW/qIv53pWusIrfVeYCSW6qhnDO7uWeAaluE2N2qt92utf9Rab8tBXPOAFKB3uo8/DnyvtU40GIdwYZL8Rb6jtb4EzMeS7B4BVmpLf+6OlDZGstY6BdgA1DH43YbA2mxOLndkvQfxA9YrIKVUXeBeLFdFQmRLBnMR+dW3wHdYqjnezGJ9CpaB09PztHdQDvY1sEMpVQnLSWC91nqPyTGJfEJK/iK/+hO4AfgDv2Sx/hyW+vH0grPZ5g2yrnfPStPUGaWUwlLqNpp4twItlVKFDH4+y7i01ruwXHH8D3gIywlRCEMk+Yt8SVtGIbobqHqbZpgrgIZKqceVUjWUUmOAFtls9ghQTykVpJTyV0rd6UrhGaVUqFIqCPgYqAxMNxj+NMAXiFBK3WONb5B1SMKcxjUDGAP4AHMM7l8ISf4i/9JaX9Vax9xm3RJgPDABy3B+VbAk3TuZgaX0vhnLlcOdThYvAy8A27HccO6ttT5hMO6TwH1AIeAvLFcCw4HbPQdwp7jmYLkyiNBaXzWyfyEAGcZRiJxQSlUBooF7tNabTQ4ntdnoMaC11nqd2fGI/ENu+AqRD1mrfkoBE4GtkvhFTkm1jxD5UwvgFNAcyw1fIXJEqn2EEMIFSclfCCFckCR/IYRwQZL8hRDCBUnyF0IIFyTJXwghXJAkfyGEcEH/D0weiT/+kGWrAAAAAElFTkSuQmCC" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "A100 I32/I32 UNIFORM\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "A100 I64/I64 UNIFORM\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 5, + "source": [ + "#### RBK\n", + "### V100\n", + "## Occupancy\n", + "# I32/I32\n", + "print(\"V100 I32/I32 UNIFORM\")\n", + "query = 'Distribution == \"UNIFORM\" and\\\n", + " Key == \"I32\" and\\\n", + " Value == \"I32\" and\\\n", + " Benchmark.str.contains(\"occupancy|distribution\")'\n", + "plot_bench(filter_bench(v100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"V100 I32/I32 GAUSSIAN\")\n", + "query = 'Distribution == \"GAUSSIAN\" and\\\n", + " Key == \"I32\" and\\\n", + " Value == \"I32\"'\n", + "plot_bench(filter_bench(v100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"V100 I32/I32 UNIQUE\")\n", + "query = 'Distribution == \"UNIQUE\" and\\\n", + " Key == \"I32\" and\\\n", + " Value == \"I32\"'\n", + "plot_bench(filter_bench(v100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"V100 I32/I32 SAME\")\n", + "query = 'Distribution == \"SAME\" and\\\n", + " Key == \"I32\" and\\\n", + " Value == \"I32\"'\n", + "plot_bench(filter_bench(v100_dfs, query), \"Occupancy\")\n", + "\n", + "# I64/I64\n", + "print(\"V100 I64/I64 UNIFORM\")\n", + "query = 'Distribution == \"UNIFORM\" and\\\n", + " Key == \"I64\" and\\\n", + " Value == \"I64\" and\\\n", + " Benchmark.str.contains(\"occupancy|distribution\")'\n", + "plot_bench(filter_bench(v100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"V100 I64/I64 GAUSSIAN\")\n", + "query = 'Distribution == \"GAUSSIAN\" and\\\n", + " Key == \"I64\" and\\\n", + " Value == \"I64\"'\n", + "plot_bench(filter_bench(v100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"V100 I64/I64 UNIQUE\")\n", + "query = 'Distribution == \"UNIQUE\" and\\\n", + " Key == \"I64\" and\\\n", + " Value == \"I64\"'\n", + "plot_bench(filter_bench(v100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"V100 I64/I64 SAME\")\n", + "query = 'Distribution == \"SAME\" and\\\n", + " Key == \"I64\" and\\\n", + " Value == \"I64\"'\n", + "plot_bench(filter_bench(v100_dfs, query), \"Occupancy\")" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "V100 I32/I32 UNIFORM\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "V100 I32/I32 GAUSSIAN\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "V100 I32/I32 UNIQUE\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAX8AAAEZCAYAAAB/6SUgAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAA0K0lEQVR4nO3deXhUVbbw4d9KCAkkgUCAhIDMyAyBBJVBTFQcsNsLSKONtqIM17nV63Ue0KuNbbcDVxuvgDO2gEirNPqpKIMCooDMo4ggowgICUPAZH1/nEqRhAynKlWpFLXe56mHqn2mVdu4zql99tlbVBVjjDGRJSrUARhjjKl6lvyNMSYCWfI3xpgIZMnfGGMikCV/Y4yJQJb8jTEmAoVN8heRV0XkZxFZ7WLd5iLyuYisFJG5ItK0KmI0xphwETbJH3gduMTlun8H3lTVrsDjwNhgBWWMMeEobJK/qs4H9hctE5HWIvL/RGSpiHwpIu09izoCX3jezwH+owpDNcaYai9skn8ZJgC3qWoGcDcw3lO+AhjseT8ISBSR5BDEZ4wx1VKNUAfgLxFJAHoD74pIYXGs59+7gRdFZDgwH9gB5Fd1jMYYU12FbfLH+dXyq6qml1ygqjvxXPl7ThJXqOqvVRqdMcZUY2Hb7KOqh4AtIvIHAHF087xvICKF3+1+4NUQhWmMMdVS2CR/EXkHWAS0E5HtIjICuBoYISIrgDWcvLGbBWwQkY1ACvBkCEI2xphqS2xIZ2OMiTxhc+VvjDEmcMLihm+DBg20RYsWfm17+PBh4uPjAxvQac7qzDdWX76x+vJNZepr6dKlv6hqw9KWhUXyb9GiBUuWLPFr27lz55KVlRXYgE5zVme+sfryjdWXbypTXyKytaxl1uxjjDERyJK/McZEIEv+xhgTgSz5G2NMBLLkb4wxEciSvzHGRCBL/sYYE4HCop//hg3gb7fgX39NJykpkNGc/qzOfGP15RurL98Eq76q7MpfRH4UES3lNauqYjDGGOOoyiv/nkB0kc+NgaXAtIo2bNcO5s7176Bz5y63pwl9ZHXmG6sv31h9+aYy9XVynqtTVVnyV9W9RT97hmQ+hIvkb4wxJrBCMqSzOPMubgY+UtVby1hnNDAaICUlJWPKlCl+HSs3N5eEhAR/Q41IVme+sfryjdWXbypTX9nZ2UtVNbO0ZaFK/hcBnwDpqrqiovUzMzPVBnarOlZnvrH68o3Vl28qObBbmck/VF09RwHfukn8xhhjAq/Kk7+INMKZbnFiVR/bGGOMIxRX/sOBPOCdEBzbGGMMVZz8PTd6RwJTVDW3Ko9tjDHmpKp+wjcLaAtcU8XHNcYYU0SVJn9VnQOU89iBMcaYqmADuxljTASy5G+MMRHIkr8xxkQgS/7GGBOBLPkbY0wEsuRvjDERyJK/McZEIEv+xhgTgSz5G2NMBLLkb4wxEciSvzHGRCBL/sYYE4HKHNhNRAoAV3M8qmp0wCIyxhgTdOWN6jmUk8k/BXgc+BewyFPWCxgIPBqs4IwxxgRHmclfVacXvheRD4H7VbXo1Iuvisg3OCeA8UGL0BhjTMC5bfM/H5hTSvkcnAlajDHGhBG3yf8XYEgp5UOAvYELxxhjTFVwO5PXI8BrIpLNyTb/c4ALgRHBCMwYY0zwuEr+qvqmiGwAbgcu9xSvA/qo6uJgBWeMMSY4XM/h60nyVwcxFmOMMVXEpwncRSQNaESJewWquiyQQRljjAkuV8lfRLoDk4H2gJRYrIA95GWMMWHE7ZX/BOAnYBSwE5dP/hpjjKme3Cb/jkB3Vd0YzGCMMcZUDbf9/FcBqcEMxBhjTNVxm/wfAJ4WkQtFJEVE6hd9BTNAY4wxgee22We2599PKd7eL9gNX2OMCTtuk392IA4mIo2Bp4ABQCLwA3CTqs4LxP6NMca44/YJ30onZxFJAhYAXwGX4YwJ1Ar4ubL7NsYY4xvXD3mJSApwC07PHwXWAC+p6h6Xu7gH2KWq1xYp2+L2+MYYYwJHVCvusi8ifYD/B+yh+GQujYCLVXVRWdsW2cdazz6a4DQj7QQmAf/QUoIQkdHAaICUlJSMKVOmuPk+p8jNzSUhIcGvbSOV1ZlvrL58Y/Xlm8rUV3Z29lJVzSx1oapW+MJJ+BOAqCJlUZ6yhS73cczzGgt0B64HcoFbK9o2IyND/TVnzhy/t41UVme+sfryjdWXbypTX8ASLSOvum32SQeGq2pBkZNGgYg8C3znch9RnkDu93z+TkTa4jQlvehyH8YYYwLAbT//g0DLUspbAr+63McuYG2JsnVAM5fbG2OMCRC3V/5TgFdE5B5goaesD/BX4B2X+1gAtCtRdiaw1eX2xhhjAsRt8r8H54GuV4tscwJ4CbjP5T6eAxaKyIPAVJx2/9txnh42xhhThdz28z8O/FlE7gdae4o3q+oRtwdS1W9FZCDwF+BhYJvn3/E+RWyMMabS3I7nnwrUUNXtOIO8FZY3BU6oy77+qjoLmOVPoMYYYwLH7Q3fycClpZRfDLwVuHCMMcZUBbfJPxOYX0r5l55lxhhjwojb5F8DiC2lPK6McmOMMdWY2+S/GLiplPJbgG8DF44xxpiq4Lar54PAFyLSFfjCU3Y+TnfNC4MRmDHGmOBxdeWvql/jDOT2IzDY89oC9FLVheVsaowxphpyPaSzqq4Arg5iLMYYY6qI2zZ/PHP33i0i40Wkgaesj4iUNuaPMcaYasxV8heRDGADzpX/SKCOZ1F/4MnghGaMMSZY3F75/x0Yp6rdgbwi5Z/gDPBmjDEmjLhN/hnAG6WU7wJSAheOMcaYquA2+R8F6pVS3h6bgN0YY8KO2+T/AfCoiBQ+zasi0gJnPP/3ghGYMcaY4HGb/O8G6gN7gdrAV8D3OLN4PRSUyIwxxgSN2/H8DwF9ReR8oAfOSWOZqs4OZnDGGGOCw/VDXgCq+gWe4R1EJCYoERljjAk6t/38bxeRK4p8fgU4KiIbRKTkvLzGGGOqObdt/rfjtPcjIv2AocAwYDnwTFAiM8YYEzRum32a4AzkBvB74F1VnSYiq3AmdDHGGBNG3F75HwIaed73Bz73vD+BM6GLMcaYMOL2yv9TYKKILAPaAB97yjtx8heBMcaYMOH2yv8WYAHQEBiiqvs95T2Ad4IRmDHGmODxpZ//baWUPxrwiIwxxgSd6/H8jTHGnD4s+RtjTASy5G+MMRHIkr8xxkSgCpO/iMSIyG4R6VSZA4nIGBHREq/dldmnMcYY/1TY20dVT4jICUADcLwNQFaRz/kB2KcxxhgfuW32eQG4X0R8GgW0FL+p6u4ir72V3J8xxhg/iGrFF/QiMhM4D2c6x9XA4aLLVfVyF/sYA9yDMwFMHrAYeEBVfyhj/dHAaICUlJSMKVOmVBhnaXJzc0lISPBr20hldeYbqy/fWH35pjL1lZ2dvVRVM0tb5jb5v1beclW93sU+LgUSgfU44wQ9hDMHcCdV3VfetpmZmbpkyZIK4yzN3LlzycrK8mvbSGV15hurL99YffmmMvUlImUmf7dP+FaY3F3s4+Oin0Xka+AH4Drg2cru3xhjjHs+dfUUkUwRuVJE4j2f4/29D6CqucAaoK0/2xtjjPGf25m8UjxX6t8A/wRSPIuexc/JXEQkDqfZZ5c/2xtjjPGf2yv/54A9QDJwpEj5u8BFbnYgIn8XkfNEpKWInA1MB+KBN3yI1xhjTAC4bbK5ALhAVQ+ISNHyzUAzl/toijP8cwOcKSG/Bs5R1a0utzfGGBMgbpN/LeB4KeUNgWNudqCqV7kNyhhjTHC5bfaZDwwv8llFJBq4l5NTOhpjjAkTbq/87wHmiUhPIBbnJm8noC7QJ0ixGWOMCRJXV/6quhboAizCmc83Dudmb3dV3Ry88IwxxgSD6z76qrobeCSIsRhjjKkirpO/iDQGbgI6eorWAv+nqjuDEZgxxpjgcfuQV3+cbp1X4vTzPwIMBb4XEVf9/I0xxlQfbq/8/xeYBPxZi4wEJyLjgHFAhyDEZowxJkjcdvVsAbyopw4B+g+geUAjMsYYE3Ruk/8SnN4+JXUBvgtcOMYYY6qC22af8cBzItIWZ1gGgHNwbgDfJyI9CldU1WWBDdEYY0yguU3+b3v+/Us5y8CZ5ze6UhEZY4wJOrfJv2VQozDGGFOl3M7kZSNvGmPMacSnmbyMMcacHiz5G2NMBLLkb4wxEciSvzHGRCC3Y/tEiUhUkc+pIjJSRGwsf2OMCUNur/xnAbcBiEgCzhO/fwPmisi1QYrNGGNMkLhN/pnAF573g4FDQCNgFHB3EOIyxhgTRG6TfwLwq+f9RcC/VPUEzgmhdRDiMsYYE0Ruk/82oI+IxAMXA595yuvjjO1vjDEmjLgd3uFZ4C0gF9gKzPeU9wNWBSEuY4wxQeR2eIeXRWQJ0Az4TFULPIs2Aw8HKzhjjDHB4csE7kuBpSXKZgU8ImOMMUHnywTuZwMX4PTyKXavQFVvD3BcxhhjgshV8heRu4Gnge+BnTjj9hcqObWjMcaYas7tlf+fgdtV9cVAHVhE7seZHOYfqnproPZrjDGmYm67etYBPgrUQUXkHGA0sDJQ+zTGGOOe2+T/DnBJIA4oInVxpn68ATgQiH0aY4zxjahW3GQvIg8CdwCf4lytnyi6XFWfdX1AkanAj6p6r4jMBVaX1uwjIqNxfh2QkpKSMWXKFLeHKCY3N5eEhAS/to1UVme+sfryjdWXbypTX9nZ2UtVNbO0ZW6T/5ZyFquqtnITiIiMAm4EzlHVE+Ul/6IyMzN1yZIlbg5xirlz55KVleXXtpHK6sw3Vl++sfryTWXqS0TKTP5uH/Kq9ATuItIO5wZvX8+4QMYYY0LEdT//Qp4hnVVVD/u4aS+gAbBGRArLooF+InIjEK+qeb7GY4wxxneuZ/ISkVtEZBtwEDgkIltF5GYfjvU+0AVIL/JaAkzxvD/uw76MMcZUgtuHvB4A7gf+DnzlKT4XeEpE6qjqUxXtQ1V/5eSw0IX7PQzsV9XVPsRsjDGmktw2+9wIjFbVd4qUfS4im3Da8StM/sYYY6oPt8m/EfBtKeXfACn+HlxVs/zd1sW+KXJv4ZTPxhgTydy2+W8EhpVSPgzYELhwAqP/m/0ZMm0Ihd1YVZUh04bQ/83+IY7MGGOqB7f9/AcD04C5wAJPcR/gPOAPqvp+kOIDIDMxUZdkZLhaV4E1P6/hlyO/0KB2A5rWbML24zu8nzs16oRd/5fv119/JSkpKdRhhA2rL99YffmmMvUl8+ZVup//DM+QzncCv/MUrwPOUtXv/IoqSATo1KiT9wSQcyyHvII8kuKS6NCwvSV+Y4zB98lcrgliLGVr1w7mznW9ugCdVIl6PAoofHTgV6JkIW3qt6FrSle6NupKl5QudE3pSoukFkSJ616vp73l9gSmT6y+fGP15ZtK1Vc59znLTP4iUl9V9xe+L2//hetVF4Vt/AAX1L+Az/d/ztlNzubi1hez6udVLN+9nPfWvod6piJIqJlA50adi50QujTqQr1a9UL5NYwxJmjKu/LfKyKNVfVn4BdKn7RFPOXRwQjOH4WJf8b6GQxuP5jbUm6j7p66zFg/gyaJTXhv6HuICLnHc1nz8xpW/byKlXtWsnLPSqavm86EZRO8+2pap6n3RNA1pStdU7rSLrkdMdExIfyGxhhTeeUl//OB/UXeh8WMXSLCobxDDG4/mOlDpzNv3jymD53OkGlDOJR3yNvdM6FmAmc3PZuzm57t3VZV2Zmzs9gJYdXPq/hs82ecKHCGI4qJiqFDww7FTgpdGnUhLTHNupIaY8JGmclfVecVeT+3SqIJkM+u/axYv34RYfrQ6RUmZxGhSZ0mNKnThEvanJy+4Hj+cTb8sqHYSWHuj3OZvHKyd536teqf8iuhU8NOxNeMD86XNMaYSnA7vEM+UNgEVLQ8GfhZVatNs0+hkom+MlflNaNr0iWlC11SujCsy8nHHfYf3c/qn1cX+5Xw6nevcviEM+adILSu37rYCaFLoy60qteK6KhqV2XGmAjitrdPWZkzlggekK1+rfr0a96Pfs37ecsKtIAtB7Z4fyUU/vv++ve9N5hrx9Smc6POxZqNuqZ0Jbl2cqi+ijEmwpSb/EXkLs9bBW4Ukdwii6NxBndbH6TYwlKURNG6fmta12/NwPYDveVHThxh7d61xX4lfLDhA1757hXvOmmJaaf8SmjfoD2xNWJD8E2MMaeziq78b/P8K8BIIL/IsuPAjziDvpkK1I6pTWZaJplpJx+2U1X2HN5T7ISwcs9Kxi0ex/F85wdVjagatEtuV+yE0DWlK03rNLUbzMYYv5Wb/Atn8BKROcBgVbUJ1wNIREhNSCU1IZWLWl/kLT+Rf4JN+zcVOyks+GkB76w+OahqUlzSKc1GnRt1JjE2MRRfxRgTZtwO75Ad7EDMSTHRMXRs2JGODTtyVeervOW/HvvVe4N51Z5VrPx5JW+ueJOc4znedVomtTzlV0Kb+m1c3WC2kVCNiRyuh3cQkTOBIUAzoGbRZap6Q4DjMqVIikuib7O+9G3W11umqmw9uPWUpqOZG2dSoAUAxNWIo1PDTsWfTUjpQqP4Rt799H+zP3Vi6zB96HTvfgufjfjs2s+q9osaY4LObVfPy4D3gO+ADJyx/Vvj9Pb5MmjRmQqJCC2SWtAiqQWXt7vcW370xFHW/bKu2K+EWZtm8dry17zrpMSneJuLDhw7wOwtsxk0dRB3pN5R7Clp+wVgzOnH7ZX/48BjqjpWRHKAPwE7gbeARcEKzvivVkwtejTuQY/GPYqV78ndw6qfV3lPCCv3rGT8t+PJy3cGwPtgwwd8vPFjjutxUuNTaRjfkCe/fJImiU1IS0xzHoJLbEJSXJKdEIwJY26Tfztgquf9CaC2qh4TkceBWcCzwQjOBF5KQgopCSlc2OpCb9lvBb/x/f7vWbF7BVe9dxXH1elp1DC+IdPXTmff0X2n7KdWjVrFTgYlTw5piWmkJaZZN1Vjqim3yT8HiPO83wW0AVZ7trehL8NcYXfSBz9/EIAL61/I7P2zaVu/LStuXEFefh47c3ay49AO59+cHew4tIMdOc7nxTsWs+PQDu+vh6Ia1G5w8sSQ2KTYyaHwfYPaDexXhDFVzG3yXwz0BdbiXOk/IyLdgEFYs0/YK20k1Dp76jBj/QyGTBvC9KHTaVWvFa3qtSp3H/uP7i/15FD4edmuZfx8+Gfvk86FakbXpHFC4zJ/RTSp43yuHVM72FVhTMRwm/zvAhI878cAicAVOHP73lXGNiZMuB0JtaJ9JNdOJrl2Ml1SupS53on8E+zK3VXmr4gVe1bw0aaPvOMjFZUUl3TqiaHE50bxjWzcJGNcqDD5i0gNoD3O1T+qegS4KchxmSrm70iovoqJjqFZ3WY0q9uszHVUlUN5h8r9FbFm7xp25+72dmctFC3RpCakVvgrok5snUp/F3suwoSzCpO/qv4mIjNwTgCn3vkzp41AjoRa2TjqxtWlblxdOjTsUOZ6+QX57Dm8p8xfERv2beCLLV9wMO/gKdsm1Eyo8FdEakJqmRP32HMRJty5bfZZgXOT98fghWKMb6Kjor29ispz+Pjhcn9FzN86n105u7wT9hQShEbxjU45OaQlpnH0t6PM3jKb/5jyH9zV+C57LsKEHbfJfwzOTd5HgaVAsQbZ6jaHrzFFxdeMp21yW9omty1znQIt4Jcjv5T5K2Lrwa0s/GnhKd1eZ26cyezvZ3O04CgZjTN4uv/Twf46xgSE2+Q/y/PvDIpP51jt5vA1xh9REkWj+EY0im9E98bdy1zv2G/H2JWzix05O9h+aDt/fO+PHC04CsDSXUtp80Ib0hLT6Ne8H+c2O5d+zfvRsWFHoiSqqr6KMa64Tf6VHthNRG4B/hNo4SlaAzyhqrPK3MiYaiauRhwt67WkRVILhkwbApx8LuLClhcyqMMgvtr2FfO3zmfK6imAM+lP32Z96desH+c2P5fuqd3LvJdgTFVxO6rnvIrXqtB24F5gExAFXAe8LyIZqroyAPs3pkqU91xE0ZvAW37dwpdbv2T+1vl8ue1LPtzwIQDxMfH0PqO395fBWU3OolZMrVB+JROBfBnVswvOlXtr4AZV3SUiA4GtqvpdRdur6gclih4UkZuAXoAlfxM23D4XUfhg3HXp1wGwK2cXX2770jkhbJvPo3MfRVFqRtekZ1pPb1NR7zN6Uzeubii/ookAbkf1vAj4EPgYOB8ovExpDQwHBvpyUBGJBv6A8+DYQl+2NaY68Oe5iMaJjRnaaShDOw0F4MDRAyz4aYH3l8HfFv6NsV+NJUqi6JbSzXsyOLf5ucWG3zYmEERVK15JZDHwhqqO94zq2U1VfxCRDGCmqpbf1+7kfrrgDAcRB+QCV5fV5i8io4HRACkpKRlTpkwpuZz4+Hiio8u/12zd7iA/P5/Dhw/j5r81QG5uLgkJCRWvaIDA1dfR/KOsPbSWVQdXsfLgStYeWktegTNe0hm1zqBrUle61e1Gl7pdSI1LrfTxQsX+vnxTmfrKzs5eqqqZpS1zm/wPA51U9ccSyb8lsE5V4yrYReF+auJMBlMXZ2KYUUCWqq4ub7vMzExdsmRJsbItW7aQmJhIcnJyuck9JyeHxMTIndpQVdm3bx85OTm0bNnS1TZz584lKysruIGdRoJVX8fzj7N051LvL4Ovtn3lfWCtWd1m3nsG/Zr3o11yu7C5yLG/L99Upr5EpMzk77bNfz/QhFMf8uqBcyPXFVU9Dnzv+bhURHoCdwIj3O6j0LFjx2jRokXY/MGHioiQnJzM3r17Qx2K8VHN6Jr0OqMXvc7oxb3cS35BPqt/Xs38rfOZv20+s3+Yzdur3gagYe2GnNv8XO8JoVtKNxvjyJTLbfL/J/A3ERmK06+/hoicB/wdeK3cLcsXhTMbmF8s8btj9XR6iI6KpltqN7qlduO2s29DVdm0f5P3BvL8rfOZsW4GAHVi69D7jN7e7qU903ra3AqmGLfJ/yHgdWArzoNdaz3//hN40s0OROQpnIfFfsIZFXQYkAVc5kvAxhiHiHBm8pmcmXwmI3o4P55/OvhTsR5FD3zxAACx0bGc0/Qc7y+DXmf0IqGmtbtHMlePHarqCVW9GjgTGIqTuNur6p9UNd/lsVKBycAG4HOgJ3Cpqn7se9i+eXrB08zZMqdY2Zwtc3h6QeUfxd+9ezdXXXUVrVu3JiMjgwEDBjBhwgR+97vfFVtv+PDhTJ/u9P8+ceIE9913H23btqVHjx706tWLjz92quHgwYNce+21tGnThtatW3Pttddy8OCpA5MZU5oz6p7BsC7DeOl3L7Hm5jXs/e+9/OvKf3Fzz5s5fOIwf/nqL1w0+SKSnkrirIlncfend/PB+g/Yd8TGbIw0rvv5A6jqZhHZ43mf6+O2w31ZP5B6pvVk6PShTBsyjeyW2czZMsf7uTJUlUGDBnHddddR2BtpxYoVfPjhh+Vu9/DDD7Nr1y5Wr15NbGwse/bsYd485zm6ESNG0LlzZ958800AHn30UUaOHMm7775bqVhNZGpQuwED2w9kYPuBAOTk5bBo+yLnvsHW+bz4zYs8s+gZADo17FRsWIomdZqEMHITbL485HUHzsQtTTyfd+LM3fu8uu1DGCR3/L87WL57eanL8vPziY52Rn+8ePLFNE5szK6cXXRo2IHH5j3GY/MeK3W79NR0nr/k+XKPO2fOHGJiYrjxxhu9Zd26dePAgQMsXry41G2OHDnCxIkT2bJlC7GxThtsSkoKQ4cO5fvvv2fp0qVMnTrVu/4jjzxCmzZt2Lx5M61bty43HmMqkhibyEWtL+Ki1hcBzlhF3+74li+3OU8iv7XyLV5a8hIALZNaensTndvsXNrUb2P3j04jbh/yehqnz/3fODltYy/gEaAxcE9QogugenH1aJzYmG0Ht9GsbjPqxVV+6uHVq1eTkZHh0zbff/89zZo1o06dUycTWbt2Lenp6cWeXYiOjiY9PZ01a9ZY8jcBF1cjzukl1PxcHjj3AX4r+I0Vu1d4u5f+e+O/eWPFGwCkJqQW617auVFnG7AujLm98h8JjFTV6UXKvhCRDcDLhDj5l3eFXtjPv7Cp5+F+D/PSkpd49LxHyW5Z6fHqSlXW1ZFdNZnqrkZUDTLSMshIy+DOXneiqqz7ZV2xHkXvrnWaIJPikujbrK/3hJDROKPcAets5rPqxZc2/9LG31mJy5vGoVS0jT+7ZTbZLbKLffZXp06dvDdxi0pOTubAgQPFyvbv30+DBg1o06YN27Zt49ChQ6dc/Xfs2JHly5dTUFBAVJRTrQUFBSxfvpyOHTv6Hacx/hIROjbsSMeGHfnPzP9EVdl6cKvzy8BzQvj3xn8DUDumNuc0PcfbvfScpudQO6Y2YDOfVUduE/ebwC2llN8EvBW4cILj253fFkv02S2zmTZkGt/u/LZS+z3//PPJy8tjwoQJ3rKVK1eyb98+du7cybp16wDYunUrK1asID09ndq1azNixAj+/Oc/c/z4cQD27t3Lu+++S5s2bejevTtPPPGEd39PPPEEPXr0oE2bNpWK1ZhAEBFaJLXg2m7XMvHyiWy4dQO7/2s37/7hXUZ0H8H+o/t5bN5jXPDmBSQ9lUTvV3pzz2f3cOTEEWasn+EdBrtwVNQ6sXVcDztiAsvtlX8sMExELga+9pSdDaQBb4vI/xauqKq3BzbEyrunz6mtUtktsyvd7CMi/Otf/+KOO+7gr3/9K3FxcbRo0YLnn3+eyZMnc/3113Ps2DFiYmKYNGkSdes6IzU+8cQTPPTQQ3Ts2JG4uDji4+N5/PHHAXjllVe47bbbvO37vXr14pVXXqlUnMYEU0pCCkM6DmFIRyex/3rsVxb+tNDbo+j5r5/3TpE5Y/0MVmxdweajm72jolrTT2i4Tf7tgWWe9809/+72vIrOsB1xp/C0tDSmTTu1y2jbtm35+uuvS9kCatasydNPP83TT5/6nEG9evWYPHlywOM0pqokxSUxoO0ABrQdAMCRE0dYvH0x87fOZ8y8MWw+uhlwxi6auXEmA9oOoEaUT73OTQC4ncwlOHdGjTGnvdoxtclqkcWL37wIQO+6vVl4cCGf/fAZ/970bxonNOb69OsZ0WMEreq1CnG0kcP1zVoRqSsimZ5XUhBjMsacRkrOfPZk+pMMbj+YvPw8zmlyDt1Tu/PUgqdo/b+t6f9Wf6aunkreb3mhDvu0V2HyF5FmIjIT2Acs9rx+EZEPRaR5+VsbYyJdyZnPAKYPnc7g9oNJqJnArKtn8eOff+SxrMfYuG8jV713FU2ebcJdn9zFur3rQhz96avcZh8RaYJzg7cA54GutZ5FnYCbgYUi0lNVdwY1SmNMWKto5rMz6p7BI+c9woPnPsjsH2YzcdlEXvjmBZ77+jn6nNGHkT1GMrTTUG/XUVN5FV35PwpsAdqq6l9U9X3P60mgrWfZo8EO0hgT/kr26imtl090VDQXt7mY6UOns+OuHTx94dPsPbKX6z+4nsbPNObmWTezbNeyU7Yzvqso+Q8AHlDVoyUXqOoRnKGebUhmY0zANYpvxH/3+W/W37KeecPncXm7y3lt+WtkTMggY0IG/7fk/zh4zEa89VdFyb8hsLmc5d971olY/gzpnJWVRbt27UhPT6dDhw7FHhIzxhQnIvRr3o+3Br3Fzrt28sKlL/BbwW/cNOsm0p5N4/oPrmfBtgX2sJiPKkr+PwPlPVra1rNORCoc0jkrK4vNmzezdOlSxo4dy549eyrc9u2332b58uUsWLCAe++91/u0rzGmbPVq1ePWs25l+X8u55uR33B1l6uZvnY6fV/rS+eXOvPcouf45cgvoQ4zLFTUz/9j4AkRuUBVi/W9EpE44H+Aj4IVnFt33AHLl5e+LD+/FtF+TGWang7PP1/+Ov4M6VxSbm4u8fHxxUbyNMaUT0To2aQnPZv05NmLn2Xq6qlMXDaRuz69i/s+v49B7QcxssdIzm95vo08WoaKkv8YYAnwvYi8CKz3lHfE6e1TA7gyaNFVc/4M6Vzo6quvJjY2lk2bNvH8889b8jfGTwk1ExjRYwQjeoxg1Z5VTFo2ibdWvsXUNVNpVa8VI7qPYHj6cNIS00IdarVSbvJX1Z0i0hsYD/wFZ95ecIZx+AS4VVV3BDfEipV3hZ6Tc5TExMQqiwXcDen89ttvk5mZyd69e+nduzeXXHIJzZvbYxPGVEaXlC6Mu3Qcf+3/V2asm8HEZRN58IsHeWTOI1x25mWM6jGKS9pcYsNJ4OIhL1X9UVUHAA2Aczyvhqo6QFV/CHaA1VmnTp1YunTpKeXlDelcUsOGDenRo4frZiJjTMXiasQxrMsw5lw3h423buTu3nezePtifv/O72nxfAse/uJhfvz1x1CHGVKuG8NU9YCqfuN57Q9mUOHCnyGdSzpy5AjfffedzdJlTJC0TW7LUxc+xU93/sSMoTPomtKVJ798klbjWnHRWxfx7pp3OZ4feR0u7LdPJfg7pDM4bf61atUiLy+P4cOH+33vwBjjTkx0DIM6DGJQh0FsO7iN1757jVe+e4Wh04fSoHYDrut2HSN7jKR9g/ahDrVKWPKvJH+GdJ47d26QozLGlKdZ3WY8mvUoD/V7iE83f8qk7yYxbvE4nln0DH2b9WVUj1EM6TjktB5OwvpAGWMiVnRUNJe2vZT3hr7HT3f+xFMXPMXu3N1c9/51pD2Txi2zbmH57uWhDjMoLPkbYwyQmpDKvX3vZeOtG5lz3Rx+d+bveOW7V+j+cncyJ2Ty8pKXOZR3KNRhBowlf2OMKUJEyGqRxeTBk9n5XzsZd8k48vLzuHHWjTR+pjE3fHADi35aFPbDSVjyN8aYMtSvVZ/bz76dlTeu5OsRX/PHzn9k2ppp9H61N11e6sLzXz/PviP7Qh2mXyz5G2NMBUSEs5uezaTLJ7Hrv3Yx4XcTiK8Zz52f3Enas2kMe28YX2z5ggItCHWorlVZ8heR+0XkWxE5JCJ7RWSmiHSuquMbY0wgJMYmMipjFItHLmbFjSsY3WM0H3//MRe8eQFnvnAmY78cy66cXaEOs0JVeeWfhTNMRG/gfOA3YLaI1K/CGAJm3759pKenk56eTmpqKk2aNPF+rl27ePew119/nVtvvRWAMWPGeNft2LEj77zzjne9rKwslixZ4v38448/0rmzc348cuQIV199NV26dKFz58707duXrVu3lhmDjRJqTPB1TenKCwNeYOddO3lr0Fs0qdOEB754gDOeO4NBUwcxa+Ms8gvyQx1mqaqsn7+qXlz0s4j8CTgI9AFmVlUcgZKcnMxyz1CiY8aMISEhgbvvvhuAhISEcre98847ufvuu9m0aRMZGRkMGTKEmJiYcrcZN24cKSkprFq1CoANGzaQmppaZgzGmKpTK6YW13S9hmu6XsPGfRuZtGwSry9/nffXv0/TOk25Pv16RnQfQfOk6jN+Vygf8krE+eVxoKIVK1TOmM618vMJ2pjOldS2bVtq167NgQMHaNSoUbnr7tq1q9jAb+3atQtqbMYY/5yZfCZP93+aJ85/gpkbZjLpu0k8Mf8Jnpj/BBe1voiRPUZyebvLqRldM6RxhjL5jwOWA4tKWygio4HRACkpKac8FVu3bl1ycnIAiD1+nKj8Mn5aqfJbWcvKUXD8OHme/VckLy+PmJgYbzxHjx6la9eu3uUHDhzg0ksvJScnp9i6y5cvp1WrVtSqVYucnBzy8/M5fPiwdz+5ubkUFBSQk5PDlVdeycCBA5k6dSrnnXcew4YNo02bNmXGUNKxY8dcP1mcm5trTyH7wOrLN5FUX8kkc2+Te7ku+To+3v0xH2//mD9s/gNJMUlcnHIxAxoPoFntZuXuI1j1FZLkLyLPAn2BvqpaamZW1QnABIDMzEzNysoqtnzdunUnh2oeP77MY+Xk5Pg9pLPb83JsbCyxsbHe49SqVYuVK1d6l7/++ussWbKExMREYmNjGT9+PP/85z/ZuHEjM2fO9G5Xo0YN4uPjvZ8TEhKIjo4mMTGRPn36sGXLFj799FNmz55NdnY2ixYtokOHDqXGUFJcXBzdu3d39X3mzp1Lyfo2ZbP68k2k1tdVXEV+QT6fbP6EScsmMX3DdKZun0q/5v0Y1WMUV3S4gloxtbzrqyoi4q2vws+BUuVdPUXkOeCPwPmROiT0nXfeyZo1a3jvvfcYMWIEx44dA04dCrrkMNAJCQkMHjyY8ePHc8011/DRRyGfRM0Y44PoqGgGtB3AjCtnsP2u7Yy9YCw7Du3gT//6E2nPpnHbR7exYvcK+r/ZnyHThngfJFNVhkwbQv83+wcslipN/iIyjpOJf31F65/uLr/8cjIzM3njjTcAp7fP5MmTvf/B33jjDbKzswFYsGCB98Rw/Phx1q5da5O/GBPGUhNSua/vfWy8bSNfXPsFl7a5lAnLJpD+cjpLdy1lxvoZDJwyEIAh04YwY/0M6sTWCdiTxVXW7CMi/wD+BAwEDohIqmdRrqrmVlUc1c0jjzzCsGHDGDVqFKNHj2b9+vV069YNESEzM5OxY8cCsHnzZm666SZUlYKCAi677DKuuOKKEEdvjKmsKIkiu2U22S2z2XdkH5NXTmbisokcOHaADzd+yLrt69h0ZBOD2w9m+tDpAWv6kaoan0JEyjrQY6o6prxtMzMztWj/d3Da/Avbu8tTmTb/04nb+oLIbZP1l9WXb6y+KqaqfL39a3q/2ttbVvBIgc+JX0SWqmpmacuqrNlHVaWM15iqisEYY8LF3xf+HYAL618IUOweQCDY2D7GGFONFN7cnbF+BoPbD+bBLg8yuP1gZqyfEdATQFgn/3AfUrWqWD0ZEz5EhEN5h7xt/ADTh05ncPvBHMo7FLA2/7CdxjEuLo59+/aRnJwc0L6vpxtVZd++fcTFxYU6FGOMS59d+1mxfv0iEtCbvRDGyb9p06Zs376dvXv3lrvesWPHIj7xxcXF0bRp01CHYYzxQclEH+iL3LBN/jExMbRs2bLC9ebOnev6yVZjjIkUYd3mb4wxxj+W/I0xJgJZ8jfGmAhUZU/4VoaI7AW2+rl5A+CXAIYTCazOfGP15RurL99Upr6aq2rD0haERfKvDBFZUtbjzaZ0Vme+sfryjdWXb4JVX9bsY4wxEciSvzHGRKBISP4TQh1AGLI6843Vl2+svnwTlPo67dv8jTHGnCoSrvyNMcaUYMnfGGMikCV/Y4yJQGGf/EXkZhHZIiLHRGSpiJxbzrpZIqKlvNpXZcyh5Et9edavKSKPe7bJE5FtInJ7VcUbaj7+fb1ext/X4aqMOdT8+BsbJiLLReSIiOwWkclF5vg+7flRX7eIyDoROSoiG0TkWr8OrKph+wKuBE4Ao4AOwAtALtCsjPWzAAU6AqlFXtGh/i7Vsb4828wAvgH6Ay2As4GsUH+X6lhfQN0Sf1epwGbgtVB/l2pcZ32AfOBOoCVwDrAM+DzU36Wa1tdNnuV/BFoBVwE5wO99Pnaov3wlK24xMLFE2SZgbBnrFyb/BqGOPUzq6yLgoNWXu/oqZfs+nr+33qH+LtW1zoC7ga0lyq4HckP9XappfS0EnitR9gzwla/HDttmHxGpCWQAn5ZY9CnQ+9QtilkiIrtE5HMRyQ5KgNWMn/U1EPgWuEtEtovIJhH5XxFJCF6k1UMl/74KjQLWqOrCQMZWXflZZwuAxiLye3E0wLma/Sh4kVYPftZXLHCsRNlR4CwRifHl+GGb/HEGO4oG9pQo34Pzc7s0u3B+Nl0BDAY2AJ9X1MZ2mvCnvloBfYFuOHV2K3AJ8HpwQqxW/KkvLxGpCwwFJgY+tGrL5zpT1UU4yf5t4DiwFxDguuCFWW348zf2CXCDiPT0nCwzgZFAjGd/roXtTF7+UNUNOAm/0CIRaQH8N/BlSIKq3qJwmi2GqepBABG5FfhERFJUteQfrTnpGpz6eyvUgVRnItIRp537f3ASW2Pgb8DLgH83Mk9v/4NzYliIc5LcA7wB3AMU+LKjcL7y/wXnRlFKifIUYLcP+1kMtA1UUNWYP/W1C9hRmPg91nn+bRbY8Kqdyv59jQLeU9X9gQ6sGvOnzu4HvlHVv6nqSlX9BLgZ+JOInO4TT/tcX6p6VFVvAGrjdMBoBvyIc9O3/AnNSwjb5K+qx4GlOL1QiuqPc1Z0Kx0nyZ3W/KyvBUBaiTb+Mz3/+ju/QliozN+XiJyF01QWSU0+/tZZbZwEWFTh57DNT25U5m9MVU+o6nZVzcdpNvu3qvp05R/yu92VvFN+JU474UicblLjcLpBNfcsfxN4s8j6d+DcxGwLdALG4jRrDA71d6mm9ZUA/AS866mvPsBq4N1Qf5fqWF9FtpsEbAx1/OFQZ8BwnK6ON+HcY+qD08lgaai/SzWtrzOBP3ly2FnAFGAf0MLXY4d1m7+qThWRZOAhnLbC1cAAVS28Ki3ZNFETpz2xKc4d8jXAZap62vcsAN/rS1VzReRCnDbZb4EDwPvAfVUWdAj58feFiCTiXIk9XmWBViN+/I297qmzW3G6LB4EvgDurbqoQ8ePv7Fo4C6gHc5Jcw5OV+IffT22jeppjDER6LRuUzPGGFM6S/7GGBOBLPkbY0wEsuRvjDERyJK/McZEIEv+xhgTgSz5G2NMBLLkb8KOiDQRkQmeYaaPi8gOEZkYAWPBGBMwlvxNWBGRlsASoDPOsL9tcEbQ7AR86xml1RhTAUv+Jtz8A2fo2gtV9XNV3aaqc4ALPeX/APCMdf5fnglo8jy/EsYW7kRE0kTkbRHZ55k7dnnhxD4iMkZEVhc9qIgMF5HcIp/HiMhqERnpmdf4qIi875mMpHCdniLyqYj8IiKHROQrEelVYr8qIqNF5F0ROSwiP4jINSXWKTVWEWkhIgWeMd2Lrj/Kc8yalaxrcxqz5G/ChojUx5lM5h+qeqToMs/n8cClIlIP+AvwMM7gfZ2AP+AMUoeIxAPzcIbEHQh0wb+xeFrg/Or4D5yTT1vg1SLLE3HG8z8XZxCu5cBHnrFcinoE+ABnJNCpwKsi0qyiWD3juXwG3FBifzcAb6kzaqQxpQv1qHb2spfbF87k8QoMKmP5IM/yfjhT3d1YxnqjcMY/L3VuYmAMsLpE2XCKzCvrWSefIhNt48x6pkDbMvYrOMOHX1OkTCkyXyvOBEtHCtdxEesQnAH34jyfO3j22TnU/73sVb1fduVvTkfHcOY6/byM5d2Blar6SyWPs0NVtxX5vBin6akDgIg0EpGXRWSjiBzESeKNOHWkxpWFb1T1N5xJORq5jPUDnCGBB3s+34AzOcrqMtY3BrBmHxNevse5qu1YxvKOnuWVVYBzlV6UT5Nje7wB9ATuxJmQOx3YjjO0eFEnSnxWXP6/qaoncMZ8v0FEauCM9f6KH7GaCGPJ34QNVd2HM8/rzSJSu+gyz+dbgI9xpprMAy4oY1ffAV2L3pwtYS+QIiJFTwDppazXRETOKPL5LJz/pwqnuuwLvKCqs1R1Dc6Vf+MyjlmWimIFZ/KYbJzpDxNxJvgwplyW/E24uRWnXXy2iJwvImeISBbOjU8BblXVHJwZkcaKyPUi0lpEzhKRmzz7+CfwM/CBiJwrIq1E5PLC3j7AXKA+8IBn2xE4beslHQXeEJF0Ty+e/wNmqeomz/KNwDUi0lFEeuIkZV9vwlYUK6q6AfgKZ6Ki6ap6yMdjmAhkyd+EFVXdDGTizML2FvADToJcB/RU1S2eVe8H/orT42cd8B7ODG6o6mHgPJwmmJk4syc9hqfJSFXX4UwrOBqnPb4/Tu+hkn7ESegzcWaf+gG4vsjyG3CmwlzqWe9Vzza+fN9yYy3iFZzmJGvyMa7YTF7G+EFExgBDVLVzqGMBEJF7gRGqemaoYzHhIazn8DUm0olIAtAc+DPwZIjDMWHEmn2MCW8vAsuABcDLIY7FhBFr9jHGmAhkV/7GGBOBLPkbY0wEsuRvjDERyJK/McZEIEv+xhgTgf4/PqFdOTP6jxoAAAAASUVORK5CYII=" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "V100 I32/I32 SAME\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "V100 I64/I64 UNIFORM\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "V100 I64/I64 GAUSSIAN\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "V100 I64/I64 UNIQUE\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "V100 I64/I64 SAME\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 6, + "source": [ + "#### RBK\n", + "### A100\n", + "## Occupancy\n", + "# I32/I32\n", + "print(\"A100 I32/I32 UNIFORM\")\n", + "query = 'Distribution == \"UNIFORM\" and\\\n", + " Key == \"I32\" and\\\n", + " Value == \"I32\" and\\\n", + " Benchmark.str.contains(\"occupancy|distribution\")'\n", + "plot_bench(filter_bench(a100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"A100 I32/I32 GAUSSIAN\")\n", + "query = 'Distribution == \"GAUSSIAN\" and\\\n", + " Key == \"I32\" and\\\n", + " Value == \"I32\"'\n", + "plot_bench(filter_bench(a100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"A100 I32/I32 UNIQUE\")\n", + "query = 'Distribution == \"UNIQUE\" and\\\n", + " Key == \"I32\" and\\\n", + " Value == \"I32\"'\n", + "plot_bench(filter_bench(a100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"A100 I32/I32 SAME\")\n", + "query = 'Distribution == \"SAME\" and\\\n", + " Key == \"I32\" and\\\n", + " Value == \"I32\"'\n", + "plot_bench(filter_bench(a100_dfs, query), \"Occupancy\")\n", + "\n", + "# I64/I64\n", + "print(\"A100 I64/I64 UNIFORM\")\n", + "query = 'Distribution == \"UNIFORM\" and\\\n", + " Key == \"I64\" and\\\n", + " Value == \"I64\" and\\\n", + " Benchmark.str.contains(\"occupancy|distribution\")'\n", + "plot_bench(filter_bench(a100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"A100 I64/I64 GAUSSIAN\")\n", + "query = 'Distribution == \"GAUSSIAN\" and\\\n", + " Key == \"I64\" and\\\n", + " Value == \"I64\"'\n", + "plot_bench(filter_bench(a100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"A100 I64/I64 UNIQUE\")\n", + "query = 'Distribution == \"UNIQUE\" and\\\n", + " Key == \"I64\" and\\\n", + " Value == \"I64\"'\n", + "plot_bench(filter_bench(a100_dfs, query), \"Occupancy\")\n", + "\n", + "print(\"A100 I64/I64 SAME\")\n", + "query = 'Distribution == \"SAME\" and\\\n", + " Key == \"I64\" and\\\n", + " Value == \"I64\"'\n", + "plot_bench(filter_bench(a100_dfs, query), \"Occupancy\")" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "A100 I32/I32 UNIFORM\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "A100 I32/I32 GAUSSIAN\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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3ixdupS1a9duc5fRv39/Zs+ezdatW8nICP6Nv3XrVmbPnk3//v0bFGtcUe8w2gGl1ZTvTNAlJSKyXSlPFhMLJ3LdsOuYWDix0phGfR1++OFs2rSJ8ePHV5R98MEHrFy5ki+++KJiyo4lS5bw/vvvk5eXR/v27TnrrLO4+OKLKS0N/tSuWLGCJ554gt69ezNw4ECuv/76iutdf/317L///vTu3btBscYVNWH8GxidsO1mlgn8mu+WaxUR2W6888U7TCycWHFHMaznMCYWTuSdL95p0HXNjH/+859MmzaNXr16MWDAAK688kq6devGQw89xBlnnEFeXh6FhYX87W9/Y4cddgCCJLDzzjvTv39/9tlnH4477riKu4377ruPBQsW0KtXL3r16sWCBQu47777GtYA9RC1S+pXwHQzGwxkEax6NwDYARiSothERFLmV0N+tU3ZsJ7DGtwlBdCtWzcmTpy4TXmfPn148803qz2nTZs23HTTTdx007aP9e6444489NBDDY6roaK+h/ERsC/wBsH63m0JBrwHunvjPtclIiJNIvI7FO7+FXB1CmMREZE0FjlhmFlX4DygfFj+I+Av7v5FKgITEZH0EqlLysxGEDxCezLBexjrgZOARWam9zBERFqAqHcYfwb+Blzs7hXrYpjZ7cDtwN4piE1ERNJI1MdqewB3JiaL0F3AHkmNSERE0lLUhDGT4CmpqvYF3kteOCIi27/6TG9eUFBA3759ycvLY++996704l+6iJow7gZuNbMrzKwg/FxBMPngnWa2f/mnrguZ2Vgz+9TMNprZLDOreQrH4PhTzGy2ma03s6/M7CEz6xIxbhGRRlXf6c0BHn74YWbPns2MGTP49a9/XfHWd7qIOobxcPj1hlr2QTCTbWY1xwBgZicTjHmMBV4Pvz5vZv3dfWk1xw8BHgQuB54CcgmS18ME81uJiKSVmqY3X716NW+99Vaka5SUlNChQ4dKM9Smg6gJo2eS6rsMmODu94bbF5rZ0QSP615ZzfEHAcvc/dZw+1Mzu4NgjXERkRpdcgnMnp3ca+blwW231X5MfaY3L3fqqaeSlZXFwoULue2227bPhOHuSxpakZm1AQYBf6yy60Xg4BpOmwHcYGbHA88STK/+E4KlYqurYwwwBoLFR4qKiuodb0lJSYPOb2nUXvGoveKJ2l477LBDxSp1paVZlJXFWiOuTqWlWyku3lTrMRs3bqS0tLQijnIbNmxgy5Ytlco3b97Mxo0bKS4upqysjPHjx7P//vvzzTffMHz4cIYOHUr37vEnBC8rK9um/ppijfN72Jir5XUm6K6q2pG3HBhe3Qnu/oaZ/YSgC6odQbwvAafXcPx4YDxAfn6+FxQU1DvYoqIiGnJ+S6P2ikftFU/U9vr444/JyckB4O67UxVNm1r3Dho0iGeffbYijnK77747xcXFlcqLi4vZfffdycnJITMzkw4dOpCTk0NOTg75+fnMnTuXAQMGxI6waj01adu2LQMHDox83eSm3yQzs/4E3U+/I7g7ORroAvy1KeMSEalJfaY3r2r9+vW89957jbqaXhSNeYfxDVBGMHCdKBf4qoZzrgTedvebw+0PzGwd8JqZ/cbdl6UmVBGR+imf3vySSy7hD3/4A23btqVHjx7cdtttFdObb9y4kdatW1ea3hyCMYx27dqxadMmRo8eXe+xkFRptITh7qVmNgsYQTDTbbkRwJM1nNaeIMkkKt9O67sjEWm56jO9+fYwphUpYZhZBoC7bw23uwDHAR+7+4wY9d0CPGhmbxMMaJ8LdAP+El73gbCe08LjnwHuNbPzgBeArsBtwLvVPYYrIiKpE/UO4zngX8DtZpZN8OZ3ByDbzM5y9weiXMTdHzezTsBvCf74zwGOSXgKq3uV4yeYWQ5wAcGiTWuAVwhW+hMRkUYUNWHkE6y6BzAKWEvwbsapBC/VRUoYAO5+N8HLd9XtK6imTO9diIikgajjANnAt+H3RwL/dPfNBP/aT69hfBERSYmoCWMpMMTMOgBHEbwLAbATwdoYIiLSzEXtkrqFYE6nEmAJ8O+w/FDgwxTEJSIiaSbSHYa7/xX4H+BMYGj501IEq/BdlaLYRES2KytXriQvL4+8vDy6dOnCrrvuWrHdvn37SsdOmDCBCy64AIBx48ZVHNu/f38effTRiuMKCgqYOXNmxfZnn33GPvvsAwQv+J166qnsu+++7LPPPgwdOpQlS5YwZMiQamNo6Oy3kd/DcPdZwKwqZc81qHYRkWakU6dOzA5nPBw3bhzZ2dlcfvnlAGRnZ9d67qWXXsrll1/OwoULGTRoEIWFhbRu3brWc26//XZyc3P58MOgo2f+/Pl06dKFGTNmkJOTs00MDRU5YZjZgQRTiu9ClTsTd78oKdGIiLRwffr0oX379qxevZpddtml1mO//PJL9tjju0VP+/btC5CydTSivrh3OXATsAj4gmDdi3JVl20VEWl6TTW/eQ02bNhQad6oVatWccIJJ2xz3LvvvkufPn3qTBYAZ555JkceeSSTJk3iiCOO4PTTT6dPnz71ii+KqHcYFwMXufudKYtERKQZa9euXUV3FQRjGIljE7feeiv/+Mc/WLBgAc8880xFuZltc63ysry8PD755BNefPFFpk2bxuDBg3njjTfYbbfdUvIzRE0YHalhDQoRkbRUzzuBplI+hvH0009z1llnsXjxYtq2bUunTp1YvXp1xXGrVq2ic+fOFdvZ2dmMGjWKUaNGkZGRwdSpUxkzZkxKYoz6HsajBFOLi4hICp1wwgnk5+dz//33A8FTUg899BDuQe///fffz7BhwwCYMWNGRTIpLS3lo48+qjSmkWxR7zD+C1wbrrH9AbA5cae735LswEREWqqrr76aU045hXPOOYcxY8Ywb9489ttvP8yM/Px8brzxRgAWL17Meeedh7uzdetWjj32WH70ox9RUlKSkrisPGvVepDZp7XsdnffM3khJUd+fr4n9g/GpRXR4lF7xaP2iifOint777136gNKc1FX3KuuvcxslrvnV3d81DW9e0Y5TkREmq/YixCZWXY4p5SIiLQgkROGmZ1vZksJ1qRYa2ZLzGxs6kITEYkvSje71K+dor649xuC9bX/CLweFh8C/J+ZdXT3/4tds4hIkrVt25aVK1fSqVOnat9fkIC7s3LlStq2bRvrvKhPSZ0LjHH3RxPKXjazhcANgBKGiDS53XbbjWXLlrFixYqmDqVJbdy4sc5k0LZt29gv+EVNGLsA71RT/jaQG6tGEZEUad26NT176hmdoqIiBg4cmPTrRh3DWACcUk35KcD85IUjIiLpKuodxjhgopkdCswIy4YAhwE/TkFcDTd/PjTgOfe8b7+F730vWdE0e2qveNRe8ai94klVe0VdQGkycCDwFXBc+PkKOMDdn0p6VCIiknbiLqD00xTGklx9+0JRUb1Pn603cWNRe8Wj9opH7RVPg9qrlqfLakwYZraTu68q/76265cfJyIizVdtXVIrzKx8BY9vgBXVfMrLIzOzsWb2qZltNLNZZnZIHce3MbPrwnM2mdlSM9MKfyIijay2LqnDgVUJ3zf49UkzOxm4HRhL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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "A100 I32/I32 UNIQUE\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "A100 I32/I32 SAME\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZUAAAEZCAYAAABfKbiYAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAAvMElEQVR4nO3de5xVdb3/8dcbuYwKkqIOIAfxIKmox0FGPYbKoJJm5TEltSwlTX6a1/yZR89JvGR5sjLNMsMsr10QreyXHa8MmZoK3kIJhBRCxRBMmQRB+Pz+WGtws5nLWjN7z8xm3s/HYz9m1nd911qf/WWzP7O+a63vVxGBmZlZKfTo7ADMzGzT4aRiZmYl46RiZmYl46RiZmYl46RiZmYl46RiZmYl0+2TiqSfSPq7pNkZ6h4k6WlJ70uaULTuJEkvpa+TyhexmVnX1e2TCnAzcHjGuouAicDPCgslbQNcAuwH7AtcImnr0oVoZlYZun1SiYg/AMsLyyQNl/S/kmZJekTSrmndVyLieWBd0W4OAx6IiOUR8RbwANkTlZnZJqNnZwfQRU0BTouIlyTtB1wPHNxC/R2AvxUsL07LzMy6FSeVIpL6Ah8B7pTUWNyn8yIyM6scTiob6wH8IyJqcmzzKlBXsDwEqC9dSGZmlaHbX1MpFhHvAC9L+jSAEnu1stl9wEclbZ1eoP9oWmZm1q10+6Qi6efA48AukhZLOgU4AThF0nPAC8B/pHX3kbQY+DTwI0kvAETEcuBrwFPp6/K0zMysW5GHvjczs1Lp9mcqZmZWOt36Qv22224bw4YNa/P2//znP9lyyy1LF9Amzu2Vj9srH7dXPu1pr1mzZr0ZEds1ta5bJ5Vhw4Yxc+bMNm9fX19PXV1d6QLaxLm98nF75eP2yqc97SVpYXPr3P1lZmYl46RiZmYl46RiZmYl062vqZhZ97NmzRoWL17MqlWrOjuUTtW/f3/mzJnTYp2qqiqGDBlCr169Mu/XScXMupXFixfTr18/hg0bRsH4ft3OihUr6NevX7PrI4Jly5axePFidtppp8z7dfeXmXUrq1atYsCAAd06oWQhiQEDBuQ+o3NSMbNuxwklm7a0U7fu/po7F9pzW/s//lHDhz5Uqmg2fW6vfNxe+WRtr0sugR7+c5r339+cnhkywJIlcPrp2ffbrZOKmVlnWbp0CVdeeS5//vNT9Ov3IbbdtppDDjmKhx++hx/96P+tr3fhhROpq/sEhx8+gTVr1vC9713M/fffxZZb9qNXrz6cccZkDjroY6xY8TZXXHEWzzzzGBHB3nuP4atfvY5+/fp36Pvq1klll12gvr7t29fXP+sneHNwe+Xj9sona3vNmZP838/iqkevYp/B+zBup3Hry6a/PJ2nXnuKC8Zc0MZIk4vgEyd+ipNOOonf/e4XADz33HPcc8899O27YXz9+8MOOyRlF154Me+99zpz586mT58+vPHGG8yYMYNddoEJE06hpmYPfv3rWwG45JJL+OY3v8idd97ZZAwrVqxs8UJ9o3XrNv6ebKlXzCeBZmbN2GfwPhw77VimvzwdSBLKsdOOZZ/B+7Rrv9OnT6dXr16cdtpp68v22msvDjzwwGa3effdd7nxxhu57rrr6NMnmYy2urqaY489lvnz5zNr1iwuvvji9fUnT57MzJkzWbBgQbtizatbn6mYWfd27v+ey7NLnm2xzuB+gzns9sMY1G8Qr694nd22243LZlzGZTMua7J+zcAarjn8mhb3OXv2bEaPHp0r1vnz5zN06FC22mqrjda9+OKL1NTUsNlmm60v22yzzaipqeGFF15g+PDhuY7VHh16piLpIEn3SHpVUkia2Er9YWm94tfhRfXGSpolaZWkv0o6rbl9mpnlsXXV1gzqN4hFby9iUL9BbF21ddmO1dzdVpV0t1pHn6n0BWYDt6avrA4HnitYXj+roqSdgHuBnwCfAw4Arpe0NCLuanfEZrbJau2MAj7o8rr4oIv54cwfcsnYSza4xtIWu+++O9OmTduofMCAAbz11lsblC1fvpxtt92WnXfemUWLFvHOO+9sdLYycuRInn32WdatW0eP9Na2devW8eyzzzJy5Mh2xZpXh56pRMS9EfFfETENWJdj02URsaTgtbpg3WnAaxFxVkTMiYgbgVuA80sZu5l1P40JZeqEqVw+7nKmTpi6wTWWtjr44IN57733mDJlyvqy559/nmXLlvHaa6+tHz5l4cKFPPfcc9TU1LDFFltwyimncM4557B6dfIVuHTpUu6880523nlnRo0axRVXXLF+f1dccQV77703O++8c7tizatSLtTfLenvkh6VNKFo3f7A/UVl9wG1krIPWGNmVuSp155i6oSp689Mxu00jqkTpvLUa0+1a7+S+NWvfsWD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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "A100 I64/I64 UNIFORM\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "A100 I64/I64 GAUSSIAN\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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itxwww0568466yz27NnDggUL8j3m4MGDvPTSS2zatInKlSsDEBMTw4ABA1i/fj2LFi3inXfeydn/vvvuIz4+ng0bNhAXF3faeIwxJctbqLDry13p904/Fly3gJa1WwY7rBLhduDeozgVa18AenmW/wL3AY8EKrhgq12lNo2iG7F131YaRTeidpXiT1++fPlykpKS/Dpm/fr1NGvWjBo1apyybeXKlSQmJhIZGZmzLjIyksTERFasWFHseI0x/gvXQoVurzCuA65T1XSfdV+KyBqcJHJHYW8gImOB/kACcARnytexqrr8NMfE4nS25/UXVf3UZez5KuxKYP/+/SzcuZAB6QO497x7eX7h89x//v30bBGYOowFPZVkTysZUzaFY6FCf/owlhawzu17pAATgW445UWOA7NdzqdxCdDIZ/nS5TmL7KutXzEgfQDTUqcxvud4pqVOy9WnUVTt2rVj0aJFp6yvW7cue/bsybVu9+7d1KtXj/j4eLZu3UpWVtYpx7Vt25YlS5Zw4sTJx/hOnDjBkiVLaNu2bbFiNcYUj7dQ4awNsxg1cxRl8O59Lm7/2L8BjMxn/Y3Am27eQFX/rKqvqepyVV0G/A2oD3R3cfguVf3NZznqMu4iW7x9MdNSp+VcUfRs0ZNpqdP44dcfivW+F1xwAUeOHOHFF1/MWbd06VJ27drFr7/+mlOOZMuWLfz0008kJiZSrVo1hg8fzq233srRo85H37FjB++++y7x8fF06tSJhx56KOf9HnroITp37kx8vE2SaEywhVOhQrcJozIwVERWi8jrnmUVkIYziO8/3sWPc0d7zr+nsB2B6SLyu4jME5FUP85RZKO7jD7l9lPPFj25o3uhd99OS0R4//33mT17NnFxcbRr146xY8fSuHFjJk+ezLBhw0hMTCQ1NZWXX36ZmjVrAk4SqF+/Pm3btqV9+/b06dMnp0/jlVdeYe3atcTFxREXF8fatWt55ZVXihWnMabkhEuhQnFziSQibu/DqKpe4OrEItOAVkByQWM5RKQeMASYh3MLqy9wNzBEVSfns/8IYARATExM0tSpU3Ntr1mzput/dWdnZ+fqSC6P1q9f73o8x4EDB4iKigpwROHD2ss/4dBeh7IPceuSW8k8lMmzic/SMipwT04Vp7169uy5SFWT892oqqW+4DyO+yvQsgjHTgSWFrZfUlKS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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "A100 I64/I64 UNIQUE\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "A100 I64/I64 SAME\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "metadata": {} + } + ], + "metadata": { + "interpreter": { + "hash": "fab55a90acef312968e5bff70ae91c3267a5b896b51d076af77c4418fdb5d582" + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3.9.5 64-bit ('base': conda)" + }, + "language_info": { + "name": "python", + "version": "3.9.5", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu index 0f481c158..20553238b 100644 --- a/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu +++ b/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu @@ -115,5 +115,6 @@ NVBENCH_BENCH_TYPES(nvbench_cub_reduce_by_key, NVBENCH_TYPE_AXES(key_type_range, .set_type_axes_names({"Key", "Value"}) .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs - .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000}) // key multiplicity range + .add_int64_axis("Multiplicity", + {1, 10, 100, 1'000, 10'000, 100'000, 1'000'000}) // key multiplicity range .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file diff --git a/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu index e5ee1a7a2..ecbdd06e4 100644 --- a/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu +++ b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu @@ -116,7 +116,7 @@ void nvbench_cuco_static_reduction_map_reduce_by_key( // type parameter dimensions for benchmark using key_type_range = nvbench::type_list; using value_type_range = nvbench::type_list; -using op_type_range = nvbench::enum_type_list; +using op_type_range = nvbench::enum_type_list; NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_reduce_by_key, NVBENCH_TYPE_AXES(key_type_range, value_type_range, op_type_range)) @@ -134,6 +134,7 @@ NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_reduce_by_key, .set_type_axes_names({"Key", "Value", "ReductionOp"}) .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs - .add_float64_axis("Occupancy", {0.8}) // fixed occupancy - .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000}) // key multiplicity range + .add_float64_axis("Occupancy", {0.5, 0.8}) // fixed occupancy + .add_int64_axis("Multiplicity", + {1, 10, 100, 1'000, 10'000, 100'000, 1'000'000}) // key multiplicity range .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file diff --git a/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu index ad1c77058..acd8d9f8d 100644 --- a/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu +++ b/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu @@ -15,20 +15,20 @@ */ #include +#include +#include #include #include #include -#include -#include -#include #include +#include /** * @brief Reduce-by-key implementation in Thrust. */ template -void thrust_reduce_by_key(KeyRandomIterator keys_begin, - KeyRandomIterator keys_end, +void thrust_reduce_by_key(KeyRandomIterator keys_begin, + KeyRandomIterator keys_end, ValueRandomIterator values_begin) { using Key = typename thrust::iterator_traits::value_type; @@ -48,14 +48,8 @@ void thrust_reduce_by_key(KeyRandomIterator keys_begin, /** * @brief A benchmark evaluating reduce-by-key performance. */ -template < - typename Key, - typename Value> -void nvbench_thrust_reduce_by_key( - nvbench::state& state, - nvbench::type_list< - Key, - Value>) +template +void nvbench_thrust_reduce_by_key(nvbench::state& state, nvbench::type_list) { auto const num_elems = state.get_int64("NumInputs"); auto const dist = state.get_string("Distribution"); @@ -69,16 +63,15 @@ void nvbench_thrust_reduce_by_key( // generate uniform random values generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); - thrust::device_vector d_keys(h_keys); + thrust::device_vector d_keys(h_keys); thrust::device_vector d_values(h_values); - state.exec( - nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { - timer.start(); - // TODO use CUDA stream provided by nvbench::launch - thrust_reduce_by_key(d_keys.begin(), d_keys.end(), d_values.begin()); - timer.stop(); - }); + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, + [&](nvbench::launch& launch, auto& timer) { + timer.start(); + thrust_reduce_by_key(d_keys.begin(), d_keys.end(), d_values.begin()); + timer.stop(); + }); } // type parameter dimensions for benchmark @@ -87,21 +80,20 @@ using value_type_range = nvbench::type_list; // benchmark setups NVBENCH_BENCH_TYPES(nvbench_thrust_reduce_by_key, - NVBENCH_TYPE_AXES(key_type_range, - value_type_range)) + NVBENCH_TYPE_AXES(key_type_range, value_type_range)) .set_name("nvbench_thrust_reduce_by_key_distribution") .set_type_axes_names({"Key", "Value"}) .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. - .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs - .add_int64_axis("Multiplicity", {8}) // only applies to uniform distribution + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_int64_axis("Multiplicity", {8}) // only applies to uniform distribution .add_string_axis("Distribution", {"GAUSSIAN", "UNIFORM", "UNIQUE", "SAME"}); NVBENCH_BENCH_TYPES(nvbench_thrust_reduce_by_key, - NVBENCH_TYPE_AXES(key_type_range, - value_type_range)) + NVBENCH_TYPE_AXES(key_type_range, value_type_range)) .set_name("nvbench_thrust_reduce_by_key_multiplicity") .set_type_axes_names({"Key", "Value"}) .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. - .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs - .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000}) // key multiplicity range + .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs + .add_int64_axis("Multiplicity", + {1, 10, 100, 1'000, 10'000, 100'000, 1'000'000}) // key multiplicity range .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file From cc853c3447a27a49b9b3564dce1bf8d5221ec42e Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Wed, 4 Aug 2021 21:22:09 +0000 Subject: [PATCH 40/69] Additional benchmark setups for static_reduction_map. --- .../hash_table/static_reduction_map_bench.cu | 132 ++++++------------ 1 file changed, 44 insertions(+), 88 deletions(-) diff --git a/benchmarks/hash_table/static_reduction_map_bench.cu b/benchmarks/hash_table/static_reduction_map_bench.cu index 0d651139c..411e08c8d 100644 --- a/benchmarks/hash_table/static_reduction_map_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_bench.cu @@ -24,7 +24,7 @@ /** * @brief Enum representation for reduction operators */ -enum class op_type { REDUCE_ADD, CUSTOM_OP }; +enum class op_type { REDUCE_ADD, CUSTOM_OP, CUSTOM_OP_NO_BACKOFF }; NVBENCH_DECLARE_ENUM_TYPE_STRINGS( // Enum type: @@ -36,6 +36,7 @@ NVBENCH_DECLARE_ENUM_TYPE_STRINGS( switch (o) { case op_type::REDUCE_ADD: return "REDUCE_ADD"; case op_type::CUSTOM_OP: return "CUSTOM_OP"; + case op_type::CUSTOM_OP_NO_BACKOFF: return "CUSTOM_OP_NO_BACKOFF"; default: return "ERROR"; } }, @@ -53,16 +54,27 @@ template struct op_type_map { }; +// Sum reduction with atomic fetch-and-add template <> struct op_type_map { template using type = cuco::reduce_add; }; +// Sum reduction with atomic compare-and-swap loop +// Note: default backoff strategy template <> struct op_type_map { template - using type = cuco::custom_op>; // sum reduction with CAS loop + using type = cuco::custom_op>; +}; + +// Sum reduction with atomic compare-and-swap loop +// Note: backoff strategy omitted +template <> +struct op_type_map { + template + using type = cuco::custom_op, 0>; }; /** @@ -79,91 +91,35 @@ void nvbench_cuco_static_reduction_map_insert( auto const dist = state.get_string("Distribution"); auto const multiplicity = state.get_int64_or_default("Multiplicity", 8); - std::vector h_keys(num_elems); - std::vector h_values(num_elems); + std::vector h_keys_in(num_elems); + std::vector h_values_in(num_elems); - generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); + generate_keys(state, dist, h_keys_in.begin(), h_keys_in.end(), multiplicity); // generate uniform random values - generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); + generate_keys(state, "UNIFORM", h_values_in.begin(), h_values_in.end(), 1); // the size of the hash table under a given target occupancy depends on the // number of unique keys in the input - std::size_t const unique = count_unique(h_keys.begin(), h_keys.end()); + std::size_t const unique = count_unique(h_keys_in.begin(), h_keys_in.end()); std::size_t const capacity = std::ceil(SDIV(unique, occupancy)); // alternative occupancy calculation based on the total number of inputs // std::size_t const capacity = num_elems / occupancy; - thrust::device_vector d_keys(h_keys); - thrust::device_vector d_values(h_values); + thrust::device_vector d_keys_in(h_keys_in); + thrust::device_vector d_values_in(h_values_in); - auto d_pairs_begin = - thrust::make_zip_iterator(thrust::make_tuple(d_keys.begin(), d_values.begin())); - auto d_pairs_end = d_pairs_begin + num_elems; + auto d_pairs_in_begin = + thrust::make_zip_iterator(thrust::make_tuple(d_keys_in.begin(), d_values_in.begin())); + auto d_pairs_in_end = d_pairs_in_begin + num_elems; state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { map_type map{capacity, -1}; timer.start(); - map.insert(d_pairs_begin, d_pairs_end, launch.get_stream()); - timer.stop(); - }); -} - -/** - * @brief A benchmark evaluating insert performance. - */ -template -void nvbench_cuco_static_reduction_map_custom_op_insert( - nvbench::state& state, - nvbench::type_list, - nvbench::enum_type>) -{ - using custom_op_type = - cuco::custom_op, BackoffBaseDelay, BackoffMaxDelay>; - using map_type = cuco::static_reduction_map; - - auto const num_elems = state.get_int64("NumInputs"); - auto const occupancy = state.get_float64("Occupancy"); - auto const dist = state.get_string("Distribution"); - auto const multiplicity = state.get_int64_or_default("Multiplicity", 8); - - std::vector h_keys(num_elems); - std::vector h_values(num_elems); - - generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); - - // generate uniform random values - generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); - - // the size of the hash table under a given target occupancy depends on the - // number of unique keys in the input - std::size_t const unique = count_unique(h_keys.begin(), h_keys.end()); - std::size_t const capacity = std::ceil(SDIV(unique, occupancy)); - - // alternative occupancy calculation based on the total number of inputs - // std::size_t const capacity = num_elems / occupancy; - - thrust::device_vector d_keys(h_keys); - thrust::device_vector d_values(h_values); - - auto d_pairs_begin = - thrust::make_zip_iterator(thrust::make_tuple(d_keys.begin(), d_values.begin())); - auto d_pairs_end = d_pairs_begin + num_elems; - - state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, - [&](nvbench::launch& launch, auto& timer) { - map_type map{capacity, -1}; - - timer.start(); - map.insert(d_pairs_begin, d_pairs_end, launch.get_stream()); + map.insert(d_pairs_in_begin, d_pairs_in_end, launch.get_stream()); timer.stop(); }); } @@ -171,11 +127,11 @@ void nvbench_cuco_static_reduction_map_custom_op_insert( // type parameter dimensions for benchmark using key_type_range = nvbench::type_list; using value_type_range = nvbench::type_list; -using op_type_range = nvbench::enum_type_list; -using base_delay_range = nvbench::enum_type_list<0, 8, 16, 32, 64, 128, 256>; -using max_delay_range = nvbench::enum_type_list<2048, 4096, 8192>; +using op_type_range = + nvbench::enum_type_list; // benchmark setups + NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_insert, NVBENCH_TYPE_AXES(key_type_range, value_type_range, op_type_range)) .set_name("cuco_static_reduction_map_insert_occupancy") @@ -184,28 +140,28 @@ NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_insert, .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs .add_float64_axis("Occupancy", nvbench::range(0.5, 0.9, 0.1)) // occupancy range .add_int64_axis("Multiplicity", {8}) // only applies to uniform distribution - .add_string_axis("Distribution", {"GAUSSIAN", "UNIFORM", "UNIQUE", "SAME"}); + .add_string_axis("Distribution", {"GAUSSIAN", "UNIFORM", "UNIQUE"}); +// Distribution "SAME" does not work with CUSTOM_OP NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_insert, - NVBENCH_TYPE_AXES(key_type_range, value_type_range, op_type_range)) - .set_name("cuco_static_reduction_map_insert_multiplicity") + NVBENCH_TYPE_AXES(key_type_range, + value_type_range, + nvbench::enum_type_list)) + .set_name("cuco_static_reduction_map_insert_occupancy") .set_type_axes_names({"Key", "Value", "ReductionOp"}) .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs - .add_float64_axis("Occupancy", {0.8}) // fixed occupancy - .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000}) // key multiplicity range - .add_string_axis("Distribution", {"UNIFORM"}); - -NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_custom_op_insert, - NVBENCH_TYPE_AXES(nvbench::type_list, - nvbench::type_list, - base_delay_range, - max_delay_range)) - .set_name("cuco_static_reduction_map_custom_op_insert_contention") - .set_type_axes_names({"Key", "Value", "BackoffBaseDelay", "BackoffMaxDelay"}) + .add_float64_axis("Occupancy", nvbench::range(0.5, 0.9, 0.1)) // occupancy range + .add_int64_axis("Multiplicity", {8}) // only applies to uniform distribution + .add_string_axis("Distribution", {"SAME"}); + +NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_insert, + NVBENCH_TYPE_AXES(key_type_range, value_type_range, op_type_range)) + .set_name("cuco_static_reduction_map_insert_multiplicity") + .set_type_axes_names({"Key", "Value", "ReductionOp"}) .set_max_noise(3) // Custom noise: 3%. By default: 0.5%. .add_int64_axis("NumInputs", {100'000'000}) // Total number of key/value pairs - .add_float64_axis("Occupancy", {0.8}) // fixed occupancy + .add_float64_axis("Occupancy", nvbench::range(0.5, 0.9, 0.1)) .add_int64_axis("Multiplicity", - {1, 10, 100, 1'000, 10'000, 100'000, 200'000}) // key multiplicity range + {1, 10, 100, 1'000, 10'000, 100'000, 1'000'000}) // key multiplicity range .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file From 28069d0f0a791f23ecd8ebe77f82eda2e1113c95 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Thu, 5 Aug 2021 22:29:09 +0000 Subject: [PATCH 41/69] Make key_generator.hpp usable from other benchmark suites. --- benchmarks/CMakeLists.txt | 5 +++-- .../hash_table/static_reduction_map_bench.cu | 7 +++++-- .../static_reduction_map_param_grid_search.cu | 7 +++++-- benchmarks/key_generator.hpp | 21 ++++++++++--------- .../reduce_by_key/cub_reduce_by_key_bench.cu | 7 +++++-- .../reduce_by_key/cuco_reduce_by_key_bench.cu | 7 +++++-- .../thrust_reduce_by_key_bench.cu | 7 +++++-- 7 files changed, 39 insertions(+), 22 deletions(-) diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 5f16ca5bf..4db4d74d6 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -33,7 +33,7 @@ function(ConfigureBench BENCH_NAME BENCH_SRC) add_executable(${BENCH_NAME} "${BENCH_SRC}") set_target_properties(${BENCH_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/gbenchmarks") + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/benchmarks") target_include_directories(${BENCH_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_compile_options(${BENCH_NAME} PRIVATE --expt-extended-lambda --expt-relaxed-constexpr -Xcompiler -Wno-subobject-linkage) @@ -49,7 +49,8 @@ function(ConfigureNVBench BENCH_NAME BENCH_SRC) add_executable(${BENCH_NAME} "${BENCH_SRC}") set_target_properties(${BENCH_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/nvbenchmarks") + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/benchmarks" + COMPILE_FLAGS -DNVBENCH_MODULE) target_include_directories(${BENCH_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") #"${NVBench_SOURCE_DIR}") diff --git a/benchmarks/hash_table/static_reduction_map_bench.cu b/benchmarks/hash_table/static_reduction_map_bench.cu index 411e08c8d..863477a7c 100644 --- a/benchmarks/hash_table/static_reduction_map_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_bench.cu @@ -94,10 +94,13 @@ void nvbench_cuco_static_reduction_map_insert( std::vector h_keys_in(num_elems); std::vector h_values_in(num_elems); - generate_keys(state, dist, h_keys_in.begin(), h_keys_in.end(), multiplicity); + if (not generate_keys(dist, h_keys_in.begin(), h_keys_in.end(), multiplicity)) { + state.skip("Invalid distribution."); + return; + } // generate uniform random values - generate_keys(state, "UNIFORM", h_values_in.begin(), h_values_in.end(), 1); + generate_keys("UNIFORM", h_values_in.begin(), h_values_in.end(), 1); // the size of the hash table under a given target occupancy depends on the // number of unique keys in the input diff --git a/benchmarks/hash_table/static_reduction_map_param_grid_search.cu b/benchmarks/hash_table/static_reduction_map_param_grid_search.cu index 41baaa872..27bcbf38d 100644 --- a/benchmarks/hash_table/static_reduction_map_param_grid_search.cu +++ b/benchmarks/hash_table/static_reduction_map_param_grid_search.cu @@ -47,10 +47,13 @@ void nvbench_cuco_static_reduction_map_custom_op_backoff_delay( std::vector h_keys(num_elems); std::vector h_values(num_elems); - generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); + if (not generate_keys(dist, h_keys.begin(), h_keys.end(), multiplicity)) { + state.skip("Invalid input distribution."); + return; + } // generate uniform random values - generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); + generate_keys("UNIFORM", h_values.begin(), h_values.end(), 1); // the size of the hash table under a given target occupancy depends on the // number of unique keys in the input diff --git a/benchmarks/key_generator.hpp b/benchmarks/key_generator.hpp index c16015866..6959e6823 100644 --- a/benchmarks/key_generator.hpp +++ b/benchmarks/key_generator.hpp @@ -18,12 +18,14 @@ #include #include -#include #include #include enum class dist_type { GAUSSIAN, GEOMETRIC, UNIFORM, UNIQUE, SAME }; +#if defined(NVBENCH_MODULE) +#include + NVBENCH_DECLARE_ENUM_TYPE_STRINGS( // Enum type: dist_type, @@ -46,10 +48,10 @@ NVBENCH_DECLARE_ENUM_TYPE_STRINGS( // input string. // Just use `[](auto) { return std::string{}; }` if you don't want these. [](auto) { return std::string{}; }) +#endif template -static void generate_keys(nvbench::state& state, - dist_type dist, +static bool generate_keys(dist_type dist, OutputIt output_begin, OutputIt output_end, std::size_t multiplicity = 8) @@ -107,15 +109,15 @@ static void generate_keys(nvbench::state& state, break; } default: { - state.skip("unknown distribution type"); - break; + return false; } } // switch + + return true; } template -static void generate_keys(nvbench::state& state, - std::string const& dist, +static bool generate_keys(std::string const& dist, OutputIt output_begin, OutputIt output_end, std::size_t multiplicity = 8) @@ -133,11 +135,10 @@ static void generate_keys(nvbench::state& state, } else if (dist == "SAME") { enum_value = dist_type::SAME; } else { - state.skip("unknown distribution type"); - return; + return false; } - generate_keys(state, enum_value, output_begin, output_end, multiplicity); + return generate_keys(enum_value, output_begin, output_end, multiplicity); } template diff --git a/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu index 20553238b..efbe7799d 100644 --- a/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu +++ b/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu @@ -32,10 +32,13 @@ void nvbench_cub_reduce_by_key(nvbench::state& state, nvbench::type_list h_keys(num_elems_in); std::vector h_values(num_elems_in); - generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); + if (not generate_keys(dist, h_keys.begin(), h_keys.end(), multiplicity)) { + state.skip("Invalid input distribution."); + return; + } // generate uniform random values - generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); + generate_keys("UNIFORM", h_values.begin(), h_values.end(), 1); // double buffer (ying/yang) thrust::device_vector d_keys_ying(h_keys); diff --git a/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu index ecbdd06e4..96d5ee2b3 100644 --- a/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu +++ b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu @@ -82,10 +82,13 @@ void nvbench_cuco_static_reduction_map_reduce_by_key( std::vector h_keys(num_elems); std::vector h_values(num_elems); - generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); + if (not generate_keys(dist, h_keys.begin(), h_keys.end(), multiplicity)) { + state.skip("Invalid input distribution."); + return; + } // generate uniform random values - generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); + generate_keys("UNIFORM", h_values.begin(), h_values.end(), 1); // the size of the hash table under a given target occupancy depends on the // number of unique keys in the input diff --git a/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu index acd8d9f8d..8cc5ef3cc 100644 --- a/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu +++ b/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu @@ -58,10 +58,13 @@ void nvbench_thrust_reduce_by_key(nvbench::state& state, nvbench::type_list h_keys(num_elems); std::vector h_values(num_elems); - generate_keys(state, dist, h_keys.begin(), h_keys.end(), multiplicity); + if (not generate_keys(dist, h_keys.begin(), h_keys.end(), multiplicity)) { + state.skip("Invalid input distribution."); + return; + } // generate uniform random values - generate_keys(state, "UNIFORM", h_values.begin(), h_values.end(), 1); + generate_keys("UNIFORM", h_values.begin(), h_values.end(), 1); thrust::device_vector d_keys(h_keys); thrust::device_vector d_values(h_values); From 26787adedfbc88a0f74d7ba0abd9d534eab99993 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Thu, 5 Aug 2021 23:39:19 +0000 Subject: [PATCH 42/69] Fix for make_from_uninitialized_slots. --- include/cuco/static_reduction_map.cuh | 34 +++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 06b9ce454..7e403e5bd 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -596,6 +596,33 @@ class static_reduction_map { return &slots_[(index + g.size()) % capacity_]; } + /** + * @brief Initializes the given array of slots to the specified values given by `k` and `v` + * using the threads in the group `g`. + * + * @note This function synchronizes the group `g`. + * + * @tparam CG The type of the cooperative thread group + * @param g The cooperative thread group used to initialize the slots + * @param slots Pointer to the array of slots to initialize + * @param num_slots Number of slots to initialize + * @param k The desired key value for each slot + * @param v The desired mapped value for each slot + */ + + template + __device__ static void initialize_slots( + CG g, pair_atomic_type* slots, std::size_t num_slots, Key k, Value v) + { + auto tid = g.thread_rank(); + while (tid < num_slots) { + new (&slots[tid].first) atomic_key_type{k}; + new (&slots[tid].second) atomic_mapped_type{v}; + tid += g.size(); + } + g.sync(); + } + public: /** * @brief Gets the maximum number of elements the hash map can hold. @@ -732,17 +759,20 @@ class static_reduction_map { : device_view_base{slots, capacity, empty_key_sentinel, reduction_op} { } + template __device__ static device_mutable_view make_from_uninitialized_slots( CG const& g, pair_atomic_type* slots, std::size_t capacity, Key empty_key_sentinel, - ReductionOp reduction_op) noexcept + ReductionOp reduction_op = {}) noexcept { - device_view_base::initialize_slots(g, slots, capacity, empty_key_sentinel, reduction_op); + device_view_base::initialize_slots( + g, slots, capacity, empty_key_sentinel, ReductionOp::identity); return device_mutable_view{slots, capacity, empty_key_sentinel, reduction_op}; } + /** * @brief Inserts the specified key/value pair into the map. * From e2a81b37de9248c8459245837bca98cb5f180b65 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Thu, 5 Aug 2021 23:46:29 +0000 Subject: [PATCH 43/69] [WIP] Added benchmark for static_reduction_map in shared memory. --- benchmarks/CMakeLists.txt | 28 ++++++ .../static_reduction_map_smem_bench.cu | 98 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 benchmarks/hash_table/static_reduction_map_smem_bench.cu diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 4db4d74d6..dcaa3ce22 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -24,6 +24,14 @@ CPMAddPackage( GIT_SHALLOW TRUE ) +# device-side benchmark tool +CPMAddPackage( + NAME cuda_benchmark + GITHUB_REPOSITORY sleeepyjack/cuda_benchmark + GIT_TAG master + GIT_SHALLOW TRUE +) + set_target_properties(benchmark PROPERTIES CXX_STANDARD 17) ################################################################################################### @@ -61,6 +69,22 @@ function(ConfigureNVBench BENCH_NAME BENCH_SRC) cuco) endfunction(ConfigureNVBench) +################################################################################################### +function(ConfigureCUDABench BENCH_NAME BENCH_SRC) + add_executable(${BENCH_NAME} "${BENCH_SRC}") + set_target_properties(${BENCH_NAME} PROPERTIES + POSITION_INDEPENDENT_CODE ON + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/benchmarks") + target_include_directories(${BENCH_NAME} PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}") + #"${NVBench_SOURCE_DIR}") + target_compile_options(${BENCH_NAME} PRIVATE --expt-extended-lambda --expt-relaxed-constexpr) + target_link_libraries(${BENCH_NAME} PRIVATE + cuda_benchmark + pthread + cuco) +endfunction(ConfigureCUDABench) + ################################################################################################### ### test sources ################################################################################## ################################################################################################### @@ -89,6 +113,10 @@ ConfigureNVBench(THRUST_RBK_BENCH "${THRUST_RBK_BENCH_SRC}") set(CUB_RBK_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/reduce_by_key/cub_reduce_by_key_bench.cu") ConfigureNVBench(CUB_RBK_BENCH "${CUB_RBK_BENCH_SRC}") +################################################################################################### +set(STATIC_REDUCTION_MAP_SMEM_BENCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/hash_table/static_reduction_map_smem_bench.cu") +ConfigureCUDABench(STATIC_REDUCTION_MAP_SMEM_BENCH "${STATIC_REDUCTION_MAP_SMEM_BENCH_SRC}") + ################################################################################################### set(STATIC_REDUCTION_MAP_PARAM_GRID_SEARCH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/hash_table/static_reduction_map_param_grid_search.cu") ConfigureNVBench(STATIC_REDUCTION_MAP_PARAM_GRID_SEARCH "${STATIC_REDUCTION_MAP_PARAM_GRID_SEARCH_SRC}") \ No newline at end of file diff --git a/benchmarks/hash_table/static_reduction_map_smem_bench.cu b/benchmarks/hash_table/static_reduction_map_smem_bench.cu new file mode 100644 index 000000000..4cdbe80e7 --- /dev/null +++ b/benchmarks/hash_table/static_reduction_map_smem_bench.cu @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2021, 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 + +template +void static_reduction_map_smem_insert_bench(cuda_benchmark::controller& controller, + std::size_t num_elems, + float occupancy, + dist_type dist, + std::size_t multiplicity = 8) +{ + using map_type = cuco::static_reduction_map, Key, Value>; + using pair_type = typename map_type::value_type; + + int dev_id; + cudaGetDevice(&dev_id); + struct cudaDeviceProp dev_props; + cudaGetDeviceProperties(&dev_props, dev_id); + std::size_t const max_smem = dev_props.sharedMemPerBlock; + std::size_t const max_capacity = max_smem / sizeof(pair_type); + + std::vector h_keys_in(num_elems); + std::vector h_values_in(num_elems); + + if (not generate_keys(dist, h_keys_in.begin(), h_keys_in.end(), multiplicity)) { + std::cerr << "[ERROR] Invalid input distribution.\n"; + return; + } + + // generate uniform random values + generate_keys("UNIFORM", h_values_in.begin(), h_values_in.end(), 1); + + // the size of the hash table under a given target occupancy depends on the + // number of unique keys in the input + std::size_t const unique = count_unique(h_keys_in.begin(), h_keys_in.end()); + std::size_t const capacity = std::ceil(SDIV(unique, occupancy)); + + if (capacity > max_capacity) { + std::cerr << "[ERROR] Not enough shared memory available. (" << capacity * sizeof(pair_type) + << ">" << max_capacity * sizeof(pair_type) << " bytes)\n"; + return; + } + + thrust::device_vector d_keys_in(h_keys_in); + thrust::device_vector d_values_in(h_values_in); + + controller.benchmark( + "static_reduction_map shared memory insert", + [=, keys_ptr = d_keys_in.data().get(), values_ptr = d_values_in.data().get()] __device__( + cuda_benchmark::state & state) { + using map_type = typename cuco::static_reduction_map, + Key, + Value, + cuda::thread_scope_block>; + using map_view_type = typename map_type::device_mutable_view; + + __shared__ typename map_type::pair_atomic_type* slots; + + auto g = cooperative_groups::this_thread_block(); + auto map = map_view_type::make_from_uninitialized_slots(g, slots, capacity, -1); + auto pair = pair_type(keys_ptr[g.thread_rank()], values_ptr[g.thread_rank()]); + + g.sync(); + + for (auto _ : state) { + map.insert(pair); + g.sync(); + } + }, + max_smem); +} + +int main() +{ + cuda_benchmark::controller controller(1024, 1); + + static_reduction_map_smem_insert_bench( + controller, 10'000, 0.8, dist_type::UNIFORM); +} \ No newline at end of file From 9807d8f20b7101c0e690423abcb4a14c778ab39f Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Fri, 6 Aug 2021 16:39:06 -0700 Subject: [PATCH 44/69] Added definition for slot_type. --- include/cuco/static_reduction_map.cuh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 7e403e5bd..247b1df18 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -270,6 +270,7 @@ class static_reduction_map { using atomic_key_type = cuda::atomic; using atomic_mapped_type = cuda::atomic; using pair_atomic_type = cuco::pair_type; + using slot_type = pair_atomic_type; using atomic_ctr_type = cuda::atomic; using allocator_type = Allocator; using slot_allocator_type = @@ -432,6 +433,7 @@ class static_reduction_map { using mapped_type = Value; using iterator = pair_atomic_type*; using const_iterator = pair_atomic_type const*; + using slot_type = slot_type; private: pair_atomic_type* slots_{}; ///< Pointer to flat slots storage @@ -741,6 +743,8 @@ class static_reduction_map { using mapped_type = typename device_view_base::mapped_type; using iterator = typename device_view_base::iterator; using const_iterator = typename device_view_base::const_iterator; + using slot_type = typename device_view_base::slot_type; + /** * @brief Construct a mutable view of the first `capacity` slots of the * slots array pointed to by `slots`. @@ -838,6 +842,8 @@ class static_reduction_map { using mapped_type = typename device_view_base::mapped_type; using iterator = typename device_view_base::iterator; using const_iterator = typename device_view_base::const_iterator; + using slot_type = typename device_view_base::slot_type; + /** * @brief Construct a view of the first `capacity` slots of the * slots array pointed to by `slots`. From 0c1bd4d98f2afeefb8ba00e2b8477a1bbe7f9aaa Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Fri, 6 Aug 2021 16:41:05 -0700 Subject: [PATCH 45/69] Change visibility of get_slots() from protected to public. (Fix for device_view ctor) --- include/cuco/static_reduction_map.cuh | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 247b1df18..a09f2a25a 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -455,26 +455,6 @@ class static_reduction_map { { } - /** - * @brief Gets the binary op - * - */ - __device__ ReductionOp get_op() const noexcept { return op_; } - - /** - * @brief Gets slots array. - * - * @return Slots array - */ - __device__ pair_atomic_type* get_slots() noexcept { return slots_; } - - /** - * @brief Gets slots array. - * - * @return Slots array - */ - __device__ pair_atomic_type const* get_slots() const noexcept { return slots_; } - /** * @brief Returns the initial slot for a given key `k` * @@ -626,6 +606,26 @@ class static_reduction_map { } public: + /** + * @brief Gets the binary op + * + */ + __device__ ReductionOp get_op() const noexcept { return op_; } + + /** + * @brief Gets slots array. + * + * @return Slots array + */ + __device__ pair_atomic_type* get_slots() noexcept { return slots_; } + + /** + * @brief Gets slots array. + * + * @return Slots array + */ + __device__ pair_atomic_type const* get_slots() const noexcept { return slots_; } + /** * @brief Gets the maximum number of elements the hash map can hold. * From 58a2ead931b7b039928f9734f53e28a9683b661f Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Fri, 6 Aug 2021 17:21:25 -0700 Subject: [PATCH 46/69] Move test helpers to util.hpp. --- tests/CMakeLists.txt | 2 + tests/dynamic_map/dynamic_map_test.cu | 95 ++++++++----------- tests/static_map/static_map_test.cu | 27 +----- .../static_reduction_map_test.cu | 26 +---- tests/util.hpp | 42 ++++++++ 5 files changed, 84 insertions(+), 108 deletions(-) create mode 100644 tests/util.hpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 45435b14e..71d3942e2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -28,6 +28,8 @@ function(ConfigureTest TEST_NAME TEST_SRC) target_link_libraries(${TEST_NAME} Catch2::Catch2 cuco CUDA::cudart) set_target_properties(${TEST_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") + target_include_directories(${TEST_NAME} PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}") target_compile_options(${TEST_NAME} PRIVATE --expt-extended-lambda --expt-relaxed-constexpr -Xcompiler -Wno-subobject-linkage) catch_discover_tests(${TEST_NAME}) endfunction(ConfigureTest) diff --git a/tests/dynamic_map/dynamic_map_test.cu b/tests/dynamic_map/dynamic_map_test.cu index 3e4b94f02..7b9fd19b7 100644 --- a/tests/dynamic_map/dynamic_map_test.cu +++ b/tests/dynamic_map/dynamic_map_test.cu @@ -14,76 +14,53 @@ * limitations under the License. */ -#include #include #include #include #include #include #include +#include -enum class dist_type { - UNIQUE, - UNIFORM, - GAUSSIAN -}; +enum class dist_type { UNIQUE, UNIFORM, GAUSSIAN }; -template -static void generate_keys(OutputIt output_begin, OutputIt output_end) { +template +static void generate_keys(OutputIt output_begin, OutputIt output_end) +{ auto num_keys = std::distance(output_begin, output_end); std::random_device rd; std::mt19937 gen{rd()}; - switch(Dist) { + switch (Dist) { case dist_type::UNIQUE: - for(auto i = 0; i < num_keys; ++i) { + for (auto i = 0; i < num_keys; ++i) { output_begin[i] = i; } break; case dist_type::UNIFORM: - for(auto i = 0; i < num_keys; ++i) { + for (auto i = 0; i < num_keys; ++i) { output_begin[i] = std::abs(static_cast(gen())); } break; case dist_type::GAUSSIAN: std::normal_distribution<> dg{1e9, 1e7}; - for(auto i = 0; i < num_keys; ++i) { + for (auto i = 0; i < num_keys; ++i) { output_begin[i] = std::abs(static_cast(dg(gen))); } break; } } -namespace { -// Thrust logical algorithms (any_of/all_of/none_of) don't work with device -// lambdas: See https://github.com/thrust/thrust/issues/1062 -template -bool all_of(Iterator begin, Iterator end, Predicate p) -{ - auto size = thrust::distance(begin, end); - return size == thrust::count_if(begin, end, p); -} - -template -bool any_of(Iterator begin, Iterator end, Predicate p) -{ - return thrust::count_if(begin, end, p) > 0; -} - -template -bool none_of(Iterator begin, Iterator end, Predicate p) -{ - return not all_of(begin, end, p); -} -} // namespace - - -TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", "", +TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", + "", ((typename T, dist_type Dist), T, Dist), - (int32_t, dist_type::UNIQUE), (int64_t, dist_type::UNIQUE), - (int32_t, dist_type::UNIFORM), (int64_t, dist_type::UNIFORM), - (int32_t, dist_type::GAUSSIAN), (int64_t, dist_type::GAUSSIAN)) + (int32_t, dist_type::UNIQUE), + (int64_t, dist_type::UNIQUE), + (int32_t, dist_type::UNIFORM), + (int64_t, dist_type::UNIFORM), + (int32_t, dist_type::GAUSSIAN), + (int64_t, dist_type::GAUSSIAN)) { using Key = T; using Value = T; @@ -91,25 +68,25 @@ TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", "", constexpr std::size_t num_keys{50'000'000}; cuco::dynamic_map map{30'000'000, -1, -1}; - std::vector h_keys( num_keys ); - std::vector h_values( num_keys ); - std::vector> h_pairs ( num_keys ); + std::vector h_keys(num_keys); + std::vector h_values(num_keys); + std::vector> h_pairs(num_keys); generate_keys(h_keys.begin(), h_keys.end()); - for(auto i = 0; i < num_keys; ++i) { - Key key = h_keys[i]; - Value val = h_keys[i]; - h_values[i] = val; - h_pairs[i].first = key; + for (auto i = 0; i < num_keys; ++i) { + Key key = h_keys[i]; + Value val = h_keys[i]; + h_values[i] = val; + h_pairs[i].first = key; h_pairs[i].second = val; } - thrust::device_vector d_keys( h_keys ); - thrust::device_vector d_values( h_values ); - thrust::device_vector> d_pairs( h_pairs ); - thrust::device_vector d_results( num_keys ); - thrust::device_vector d_contained( num_keys ); + thrust::device_vector d_keys(h_keys); + thrust::device_vector d_values(h_values); + thrust::device_vector> d_pairs(h_pairs); + thrust::device_vector d_results(num_keys); + thrust::device_vector d_contained(num_keys); // bulk function test cases SECTION("All inserted keys-value pairs should be correctly recovered during find") @@ -118,8 +95,7 @@ TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", "", map.find(d_keys.begin(), d_keys.end(), d_results.begin()); auto zip = thrust::make_zip_iterator(thrust::make_tuple(d_results.begin(), d_values.begin())); - REQUIRE(all_of(zip, zip + num_keys, - [] __device__(auto const& p) { + REQUIRE(all_of(zip, zip + num_keys, [] __device__(auto const& p) { return thrust::get<0>(p) == thrust::get<1>(p); })); } @@ -128,7 +104,8 @@ TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", "", { map.find(d_keys.begin(), d_keys.end(), d_results.begin()); - REQUIRE(all_of(d_results.begin(), d_results.end(), [] __device__(auto const& p) { return p == -1; })); + REQUIRE( + all_of(d_results.begin(), d_results.end(), [] __device__(auto const& p) { return p == -1; })); } SECTION("All inserted keys-value pairs should be contained") @@ -136,13 +113,15 @@ TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", "", map.insert(d_pairs.begin(), d_pairs.end()); map.contains(d_keys.begin(), d_keys.end(), d_contained.begin()); - REQUIRE(all_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); + REQUIRE( + all_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); } SECTION("Non-inserted keys-value pairs should not be contained") { map.contains(d_keys.begin(), d_keys.end(), d_contained.begin()); - REQUIRE(none_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); + REQUIRE( + none_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); } } \ No newline at end of file diff --git a/tests/static_map/static_map_test.cu b/tests/static_map/static_map_test.cu index b52f3f367..ea68981ba 100644 --- a/tests/static_map/static_map_test.cu +++ b/tests/static_map/static_map_test.cu @@ -14,7 +14,6 @@ * limitations under the License. */ -#include #include #include #include @@ -22,31 +21,7 @@ #include #include #include - -namespace { -namespace cg = cooperative_groups; - -// Thrust logical algorithms (any_of/all_of/none_of) don't work with device -// lambdas: See https://github.com/thrust/thrust/issues/1062 -template -bool all_of(Iterator begin, Iterator end, Predicate p) -{ - auto size = thrust::distance(begin, end); - return size == thrust::count_if(begin, end, p); -} - -template -bool any_of(Iterator begin, Iterator end, Predicate p) -{ - return thrust::count_if(begin, end, p) > 0; -} - -template -bool none_of(Iterator begin, Iterator end, Predicate p) -{ - return not all_of(begin, end, p); -} -} // namespace +#include enum class dist_type { UNIQUE, UNIFORM, GAUSSIAN }; diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index bb57f0847..023cb7a7b 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,36 +14,14 @@ * limitations under the License. */ -#include #include #include #include #include #include #include +#include -namespace { -// Thrust logical algorithms (any_of/all_of/none_of) don't work with device -// lambdas: See https://github.com/thrust/thrust/issues/1062 -template -bool all_of(Iterator begin, Iterator end, Predicate p) -{ - auto size = thrust::distance(begin, end); - return size == thrust::count_if(begin, end, p); -} - -template -bool any_of(Iterator begin, Iterator end, Predicate p) -{ - return thrust::count_if(begin, end, p) > 0; -} - -template -bool none_of(Iterator begin, Iterator end, Predicate p) -{ - return not all_of(begin, end, p); -} -} // namespace TEMPLATE_TEST_CASE_SIG("Insert all identical keys", "", diff --git a/tests/util.hpp b/tests/util.hpp new file mode 100644 index 000000000..bb10a7a1f --- /dev/null +++ b/tests/util.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020-2021, 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 + +namespace cg = cooperative_groups; + +// Thrust logical algorithms (any_of/all_of/none_of) don't work with device +// lambdas: See https://github.com/thrust/thrust/issues/1062 +template +bool all_of(Iterator begin, Iterator end, Predicate p) +{ + auto size = thrust::distance(begin, end); + return size == thrust::count_if(begin, end, p); +} + +template +bool any_of(Iterator begin, Iterator end, Predicate p) +{ + return thrust::count_if(begin, end, p) > 0; +} + +template +bool none_of(Iterator begin, Iterator end, Predicate p) +{ + return not all_of(begin, end, p); +} \ No newline at end of file From f4979b1bd8a10489b0fd35a43690828e375f1e15 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Fri, 6 Aug 2021 17:22:31 -0700 Subject: [PATCH 47/69] Added tests for custom_op and shared memory hash table. --- .../static_reduction_map_test.cu | 62 +++++++++++++++++-- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index 023cb7a7b..51fee5740 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -16,23 +16,28 @@ #include #include +#include #include #include #include #include #include +// cuco::custom op functor that should give the same result as cuco::reduce_add +template +using custom_reduce_add = cuco::custom_op, 0>; TEMPLATE_TEST_CASE_SIG("Insert all identical keys", "", - ((typename Key, typename Value), Key, Value), - (int32_t, int32_t)) + ((typename Key, typename Value, typename Op), Key, Value, Op), + (int32_t, int32_t, cuco::reduce_add), + (int32_t, int32_t, custom_reduce_add)) { thrust::device_vector keys(100, 42); thrust::device_vector values(keys.size(), 1); auto const num_slots{keys.size() * 2}; - cuco::static_reduction_map, Key, Value> map{num_slots, -1}; + cuco::static_reduction_map map{num_slots, -1}; auto zip = thrust::make_zip_iterator(thrust::make_tuple(keys.begin(), values.begin())); auto zip_end = zip + keys.size(); @@ -62,12 +67,13 @@ TEMPLATE_TEST_CASE_SIG("Insert all identical keys", TEMPLATE_TEST_CASE_SIG("Insert all unique keys", "", - ((typename Key, typename Value), Key, Value), - (int32_t, int32_t)) + ((typename Key, typename Value, typename Op), Key, Value, Op), + (int32_t, int32_t, cuco::reduce_add), + (int32_t, int32_t, custom_reduce_add)) { constexpr std::size_t num_keys = 10000; constexpr std::size_t num_slots{num_keys * 2}; - cuco::static_reduction_map, Key, Value> map{num_slots, -1}; + cuco::static_reduction_map map{num_slots, -1}; auto keys_begin = thrust::make_counting_iterator(0); auto values_begin = thrust::make_counting_iterator(0); @@ -94,3 +100,47 @@ TEMPLATE_TEST_CASE_SIG("Insert all unique keys", REQUIRE(thrust::equal(thrust::device, values_begin, values_begin + num_keys, found.begin())); } } + +template +__global__ void static_reduction_map_shared_memory_kernel(bool* key_found) +{ + using Key = typename MapType::key_type; + using Value = typename MapType::mapped_type; + + namespace cg = cooperative_groups; + using mutable_view_type = typename MapType::device_mutable_view; + using view_type = typename MapType::device_view; + __shared__ typename mutable_view_type::slot_type slots[N]; + auto map = + mutable_view_type::make_from_uninitialized_slots(cg::this_thread_block(), &slots[0], N, -1); + + auto g = cg::this_thread_block(); + std::size_t index = threadIdx.x + blockIdx.x * blockDim.x; + int rank = g.thread_rank(); + + // insert {thread_rank, thread_rank} for each thread in thread-block + map.insert(cuco::pair(rank, rank)); + g.sync(); + + auto find_map = view_type(map); + auto retrieved_pair = find_map.find(rank); + if (retrieved_pair != find_map.end() && retrieved_pair->second == rank) { + key_found[index] = true; + } +} + +TEMPLATE_TEST_CASE_SIG("Shared memory hast table.", + "", + ((typename Key, typename Value, typename Op), Key, Value, Op), + (int32_t, int32_t, cuco::reduce_add), + (int32_t, int32_t, custom_reduce_add)) +{ + constexpr std::size_t N = 256; + thrust::device_vector key_found(N, false); + + static_reduction_map_shared_memory_kernel< + cuco::static_reduction_map, + N><<<8, 32>>>(key_found.data().get()); + + REQUIRE(all_of(key_found.begin(), key_found.end(), thrust::identity{})); +} From 01e75bda11af7b59b7a8eef0971f2eec880768a1 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Fri, 6 Aug 2021 16:39:06 -0700 Subject: [PATCH 48/69] Added definition for slot_type. --- include/cuco/static_reduction_map.cuh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 7e403e5bd..247b1df18 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -270,6 +270,7 @@ class static_reduction_map { using atomic_key_type = cuda::atomic; using atomic_mapped_type = cuda::atomic; using pair_atomic_type = cuco::pair_type; + using slot_type = pair_atomic_type; using atomic_ctr_type = cuda::atomic; using allocator_type = Allocator; using slot_allocator_type = @@ -432,6 +433,7 @@ class static_reduction_map { using mapped_type = Value; using iterator = pair_atomic_type*; using const_iterator = pair_atomic_type const*; + using slot_type = slot_type; private: pair_atomic_type* slots_{}; ///< Pointer to flat slots storage @@ -741,6 +743,8 @@ class static_reduction_map { using mapped_type = typename device_view_base::mapped_type; using iterator = typename device_view_base::iterator; using const_iterator = typename device_view_base::const_iterator; + using slot_type = typename device_view_base::slot_type; + /** * @brief Construct a mutable view of the first `capacity` slots of the * slots array pointed to by `slots`. @@ -838,6 +842,8 @@ class static_reduction_map { using mapped_type = typename device_view_base::mapped_type; using iterator = typename device_view_base::iterator; using const_iterator = typename device_view_base::const_iterator; + using slot_type = typename device_view_base::slot_type; + /** * @brief Construct a view of the first `capacity` slots of the * slots array pointed to by `slots`. From 1e866591f320edfaa436967a856abb4c4b96505c Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Fri, 6 Aug 2021 16:41:05 -0700 Subject: [PATCH 49/69] Change visibility of get_slots() from protected to public. (Fix for device_view ctor) --- include/cuco/static_reduction_map.cuh | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 247b1df18..a09f2a25a 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -455,26 +455,6 @@ class static_reduction_map { { } - /** - * @brief Gets the binary op - * - */ - __device__ ReductionOp get_op() const noexcept { return op_; } - - /** - * @brief Gets slots array. - * - * @return Slots array - */ - __device__ pair_atomic_type* get_slots() noexcept { return slots_; } - - /** - * @brief Gets slots array. - * - * @return Slots array - */ - __device__ pair_atomic_type const* get_slots() const noexcept { return slots_; } - /** * @brief Returns the initial slot for a given key `k` * @@ -626,6 +606,26 @@ class static_reduction_map { } public: + /** + * @brief Gets the binary op + * + */ + __device__ ReductionOp get_op() const noexcept { return op_; } + + /** + * @brief Gets slots array. + * + * @return Slots array + */ + __device__ pair_atomic_type* get_slots() noexcept { return slots_; } + + /** + * @brief Gets slots array. + * + * @return Slots array + */ + __device__ pair_atomic_type const* get_slots() const noexcept { return slots_; } + /** * @brief Gets the maximum number of elements the hash map can hold. * From 21be2e1353bc2b5a5c77429ae10655ed313b20be Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Fri, 6 Aug 2021 17:21:25 -0700 Subject: [PATCH 50/69] Move test helpers to util.hpp. --- tests/CMakeLists.txt | 2 + tests/dynamic_map/dynamic_map_test.cu | 95 ++++++++----------- tests/static_map/static_map_test.cu | 27 +----- .../static_reduction_map_test.cu | 26 +---- tests/util.hpp | 42 ++++++++ 5 files changed, 84 insertions(+), 108 deletions(-) create mode 100644 tests/util.hpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 45435b14e..71d3942e2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -28,6 +28,8 @@ function(ConfigureTest TEST_NAME TEST_SRC) target_link_libraries(${TEST_NAME} Catch2::Catch2 cuco CUDA::cudart) set_target_properties(${TEST_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") + target_include_directories(${TEST_NAME} PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}") target_compile_options(${TEST_NAME} PRIVATE --expt-extended-lambda --expt-relaxed-constexpr -Xcompiler -Wno-subobject-linkage) catch_discover_tests(${TEST_NAME}) endfunction(ConfigureTest) diff --git a/tests/dynamic_map/dynamic_map_test.cu b/tests/dynamic_map/dynamic_map_test.cu index 3e4b94f02..7b9fd19b7 100644 --- a/tests/dynamic_map/dynamic_map_test.cu +++ b/tests/dynamic_map/dynamic_map_test.cu @@ -14,76 +14,53 @@ * limitations under the License. */ -#include #include #include #include #include #include #include +#include -enum class dist_type { - UNIQUE, - UNIFORM, - GAUSSIAN -}; +enum class dist_type { UNIQUE, UNIFORM, GAUSSIAN }; -template -static void generate_keys(OutputIt output_begin, OutputIt output_end) { +template +static void generate_keys(OutputIt output_begin, OutputIt output_end) +{ auto num_keys = std::distance(output_begin, output_end); std::random_device rd; std::mt19937 gen{rd()}; - switch(Dist) { + switch (Dist) { case dist_type::UNIQUE: - for(auto i = 0; i < num_keys; ++i) { + for (auto i = 0; i < num_keys; ++i) { output_begin[i] = i; } break; case dist_type::UNIFORM: - for(auto i = 0; i < num_keys; ++i) { + for (auto i = 0; i < num_keys; ++i) { output_begin[i] = std::abs(static_cast(gen())); } break; case dist_type::GAUSSIAN: std::normal_distribution<> dg{1e9, 1e7}; - for(auto i = 0; i < num_keys; ++i) { + for (auto i = 0; i < num_keys; ++i) { output_begin[i] = std::abs(static_cast(dg(gen))); } break; } } -namespace { -// Thrust logical algorithms (any_of/all_of/none_of) don't work with device -// lambdas: See https://github.com/thrust/thrust/issues/1062 -template -bool all_of(Iterator begin, Iterator end, Predicate p) -{ - auto size = thrust::distance(begin, end); - return size == thrust::count_if(begin, end, p); -} - -template -bool any_of(Iterator begin, Iterator end, Predicate p) -{ - return thrust::count_if(begin, end, p) > 0; -} - -template -bool none_of(Iterator begin, Iterator end, Predicate p) -{ - return not all_of(begin, end, p); -} -} // namespace - - -TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", "", +TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", + "", ((typename T, dist_type Dist), T, Dist), - (int32_t, dist_type::UNIQUE), (int64_t, dist_type::UNIQUE), - (int32_t, dist_type::UNIFORM), (int64_t, dist_type::UNIFORM), - (int32_t, dist_type::GAUSSIAN), (int64_t, dist_type::GAUSSIAN)) + (int32_t, dist_type::UNIQUE), + (int64_t, dist_type::UNIQUE), + (int32_t, dist_type::UNIFORM), + (int64_t, dist_type::UNIFORM), + (int32_t, dist_type::GAUSSIAN), + (int64_t, dist_type::GAUSSIAN)) { using Key = T; using Value = T; @@ -91,25 +68,25 @@ TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", "", constexpr std::size_t num_keys{50'000'000}; cuco::dynamic_map map{30'000'000, -1, -1}; - std::vector h_keys( num_keys ); - std::vector h_values( num_keys ); - std::vector> h_pairs ( num_keys ); + std::vector h_keys(num_keys); + std::vector h_values(num_keys); + std::vector> h_pairs(num_keys); generate_keys(h_keys.begin(), h_keys.end()); - for(auto i = 0; i < num_keys; ++i) { - Key key = h_keys[i]; - Value val = h_keys[i]; - h_values[i] = val; - h_pairs[i].first = key; + for (auto i = 0; i < num_keys; ++i) { + Key key = h_keys[i]; + Value val = h_keys[i]; + h_values[i] = val; + h_pairs[i].first = key; h_pairs[i].second = val; } - thrust::device_vector d_keys( h_keys ); - thrust::device_vector d_values( h_values ); - thrust::device_vector> d_pairs( h_pairs ); - thrust::device_vector d_results( num_keys ); - thrust::device_vector d_contained( num_keys ); + thrust::device_vector d_keys(h_keys); + thrust::device_vector d_values(h_values); + thrust::device_vector> d_pairs(h_pairs); + thrust::device_vector d_results(num_keys); + thrust::device_vector d_contained(num_keys); // bulk function test cases SECTION("All inserted keys-value pairs should be correctly recovered during find") @@ -118,8 +95,7 @@ TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", "", map.find(d_keys.begin(), d_keys.end(), d_results.begin()); auto zip = thrust::make_zip_iterator(thrust::make_tuple(d_results.begin(), d_values.begin())); - REQUIRE(all_of(zip, zip + num_keys, - [] __device__(auto const& p) { + REQUIRE(all_of(zip, zip + num_keys, [] __device__(auto const& p) { return thrust::get<0>(p) == thrust::get<1>(p); })); } @@ -128,7 +104,8 @@ TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", "", { map.find(d_keys.begin(), d_keys.end(), d_results.begin()); - REQUIRE(all_of(d_results.begin(), d_results.end(), [] __device__(auto const& p) { return p == -1; })); + REQUIRE( + all_of(d_results.begin(), d_results.end(), [] __device__(auto const& p) { return p == -1; })); } SECTION("All inserted keys-value pairs should be contained") @@ -136,13 +113,15 @@ TEMPLATE_TEST_CASE_SIG("Unique sequence of keys", "", map.insert(d_pairs.begin(), d_pairs.end()); map.contains(d_keys.begin(), d_keys.end(), d_contained.begin()); - REQUIRE(all_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); + REQUIRE( + all_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); } SECTION("Non-inserted keys-value pairs should not be contained") { map.contains(d_keys.begin(), d_keys.end(), d_contained.begin()); - REQUIRE(none_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); + REQUIRE( + none_of(d_contained.begin(), d_contained.end(), [] __device__(bool const& b) { return b; })); } } \ No newline at end of file diff --git a/tests/static_map/static_map_test.cu b/tests/static_map/static_map_test.cu index b52f3f367..ea68981ba 100644 --- a/tests/static_map/static_map_test.cu +++ b/tests/static_map/static_map_test.cu @@ -14,7 +14,6 @@ * limitations under the License. */ -#include #include #include #include @@ -22,31 +21,7 @@ #include #include #include - -namespace { -namespace cg = cooperative_groups; - -// Thrust logical algorithms (any_of/all_of/none_of) don't work with device -// lambdas: See https://github.com/thrust/thrust/issues/1062 -template -bool all_of(Iterator begin, Iterator end, Predicate p) -{ - auto size = thrust::distance(begin, end); - return size == thrust::count_if(begin, end, p); -} - -template -bool any_of(Iterator begin, Iterator end, Predicate p) -{ - return thrust::count_if(begin, end, p) > 0; -} - -template -bool none_of(Iterator begin, Iterator end, Predicate p) -{ - return not all_of(begin, end, p); -} -} // namespace +#include enum class dist_type { UNIQUE, UNIFORM, GAUSSIAN }; diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index bb57f0847..023cb7a7b 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,36 +14,14 @@ * limitations under the License. */ -#include #include #include #include #include #include #include +#include -namespace { -// Thrust logical algorithms (any_of/all_of/none_of) don't work with device -// lambdas: See https://github.com/thrust/thrust/issues/1062 -template -bool all_of(Iterator begin, Iterator end, Predicate p) -{ - auto size = thrust::distance(begin, end); - return size == thrust::count_if(begin, end, p); -} - -template -bool any_of(Iterator begin, Iterator end, Predicate p) -{ - return thrust::count_if(begin, end, p) > 0; -} - -template -bool none_of(Iterator begin, Iterator end, Predicate p) -{ - return not all_of(begin, end, p); -} -} // namespace TEMPLATE_TEST_CASE_SIG("Insert all identical keys", "", diff --git a/tests/util.hpp b/tests/util.hpp new file mode 100644 index 000000000..bb10a7a1f --- /dev/null +++ b/tests/util.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020-2021, 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 + +namespace cg = cooperative_groups; + +// Thrust logical algorithms (any_of/all_of/none_of) don't work with device +// lambdas: See https://github.com/thrust/thrust/issues/1062 +template +bool all_of(Iterator begin, Iterator end, Predicate p) +{ + auto size = thrust::distance(begin, end); + return size == thrust::count_if(begin, end, p); +} + +template +bool any_of(Iterator begin, Iterator end, Predicate p) +{ + return thrust::count_if(begin, end, p) > 0; +} + +template +bool none_of(Iterator begin, Iterator end, Predicate p) +{ + return not all_of(begin, end, p); +} \ No newline at end of file From 53bfe278387c8088920cecd32a2d7950154642e0 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Fri, 6 Aug 2021 17:22:31 -0700 Subject: [PATCH 51/69] Added tests for custom_op and shared memory hash table. --- .../static_reduction_map_test.cu | 62 +++++++++++++++++-- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index 023cb7a7b..51fee5740 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -16,23 +16,28 @@ #include #include +#include #include #include #include #include #include +// cuco::custom op functor that should give the same result as cuco::reduce_add +template +using custom_reduce_add = cuco::custom_op, 0>; TEMPLATE_TEST_CASE_SIG("Insert all identical keys", "", - ((typename Key, typename Value), Key, Value), - (int32_t, int32_t)) + ((typename Key, typename Value, typename Op), Key, Value, Op), + (int32_t, int32_t, cuco::reduce_add), + (int32_t, int32_t, custom_reduce_add)) { thrust::device_vector keys(100, 42); thrust::device_vector values(keys.size(), 1); auto const num_slots{keys.size() * 2}; - cuco::static_reduction_map, Key, Value> map{num_slots, -1}; + cuco::static_reduction_map map{num_slots, -1}; auto zip = thrust::make_zip_iterator(thrust::make_tuple(keys.begin(), values.begin())); auto zip_end = zip + keys.size(); @@ -62,12 +67,13 @@ TEMPLATE_TEST_CASE_SIG("Insert all identical keys", TEMPLATE_TEST_CASE_SIG("Insert all unique keys", "", - ((typename Key, typename Value), Key, Value), - (int32_t, int32_t)) + ((typename Key, typename Value, typename Op), Key, Value, Op), + (int32_t, int32_t, cuco::reduce_add), + (int32_t, int32_t, custom_reduce_add)) { constexpr std::size_t num_keys = 10000; constexpr std::size_t num_slots{num_keys * 2}; - cuco::static_reduction_map, Key, Value> map{num_slots, -1}; + cuco::static_reduction_map map{num_slots, -1}; auto keys_begin = thrust::make_counting_iterator(0); auto values_begin = thrust::make_counting_iterator(0); @@ -94,3 +100,47 @@ TEMPLATE_TEST_CASE_SIG("Insert all unique keys", REQUIRE(thrust::equal(thrust::device, values_begin, values_begin + num_keys, found.begin())); } } + +template +__global__ void static_reduction_map_shared_memory_kernel(bool* key_found) +{ + using Key = typename MapType::key_type; + using Value = typename MapType::mapped_type; + + namespace cg = cooperative_groups; + using mutable_view_type = typename MapType::device_mutable_view; + using view_type = typename MapType::device_view; + __shared__ typename mutable_view_type::slot_type slots[N]; + auto map = + mutable_view_type::make_from_uninitialized_slots(cg::this_thread_block(), &slots[0], N, -1); + + auto g = cg::this_thread_block(); + std::size_t index = threadIdx.x + blockIdx.x * blockDim.x; + int rank = g.thread_rank(); + + // insert {thread_rank, thread_rank} for each thread in thread-block + map.insert(cuco::pair(rank, rank)); + g.sync(); + + auto find_map = view_type(map); + auto retrieved_pair = find_map.find(rank); + if (retrieved_pair != find_map.end() && retrieved_pair->second == rank) { + key_found[index] = true; + } +} + +TEMPLATE_TEST_CASE_SIG("Shared memory hast table.", + "", + ((typename Key, typename Value, typename Op), Key, Value, Op), + (int32_t, int32_t, cuco::reduce_add), + (int32_t, int32_t, custom_reduce_add)) +{ + constexpr std::size_t N = 256; + thrust::device_vector key_found(N, false); + + static_reduction_map_shared_memory_kernel< + cuco::static_reduction_map, + N><<<8, 32>>>(key_found.data().get()); + + REQUIRE(all_of(key_found.begin(), key_found.end(), thrust::identity{})); +} From f4c703a502702d42540b882380385421e09a3af6 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Sat, 7 Aug 2021 18:33:38 -0700 Subject: [PATCH 52/69] Added benchmark for static_reduction_map in shared memory. --- benchmarks/CMakeLists.txt | 1 + .../static_reduction_map_smem_bench.cu | 164 ++++++++++++------ 2 files changed, 113 insertions(+), 52 deletions(-) diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index dcaa3ce22..d938959a2 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -81,6 +81,7 @@ function(ConfigureCUDABench BENCH_NAME BENCH_SRC) target_compile_options(${BENCH_NAME} PRIVATE --expt-extended-lambda --expt-relaxed-constexpr) target_link_libraries(${BENCH_NAME} PRIVATE cuda_benchmark + fmt pthread cuco) endfunction(ConfigureCUDABench) diff --git a/benchmarks/hash_table/static_reduction_map_smem_bench.cu b/benchmarks/hash_table/static_reduction_map_smem_bench.cu index 4cdbe80e7..147c89c92 100644 --- a/benchmarks/hash_table/static_reduction_map_smem_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_smem_bench.cu @@ -15,84 +15,144 @@ */ #include -#include #include #include -#include +#include #include +#include "fmt/core.h" +#include "fmt/format.h" +template +std::string get_type_str(); + +template <> +std::string get_type_str() +{ + return "U32"; +} +template <> +std::string get_type_str() +{ + return "U64"; +} + +/** + * @brief Device-side benchmark for shared memory reduction hash table insert. + * + * @tparam Key The hash table's key type + * @tparam Value The hash table's value/reduction type + * @param controller Benchmark controller/state handler + * @param bench_name Benchmark identifier + * @param num_elems_log2 Total number of key/value pairs to be inserted (log2) + * @param multiplicity_log2 Number of times each key occures in the input (log2) + * @param occupancy Target occupancy of the hash table after inserting all elements + */ template -void static_reduction_map_smem_insert_bench(cuda_benchmark::controller& controller, - std::size_t num_elems, - float occupancy, - dist_type dist, - std::size_t multiplicity = 8) +void static_reduction_map_smem_insert_bench(cuda_benchmark::controller &controller, + std::string const &bench_name, + std::uint32_t num_elems_log2, + std::uint32_t multiplicity_log2, + float occupancy) { using map_type = cuco::static_reduction_map, Key, Value>; using pair_type = typename map_type::value_type; - int dev_id; - cudaGetDevice(&dev_id); - struct cudaDeviceProp dev_props; - cudaGetDeviceProperties(&dev_props, dev_id); - std::size_t const max_smem = dev_props.sharedMemPerBlock; - std::size_t const max_capacity = max_smem / sizeof(pair_type); + auto const num_elems = 1UL << num_elems_log2; + auto const multiplicity = 1UL << multiplicity_log2; - std::vector h_keys_in(num_elems); - std::vector h_values_in(num_elems); + std::string full_bench_name = "INSERT " + bench_name + " key_type=" + get_type_str() + + " value_type=" + get_type_str() + + " num_elems=" + std::to_string(num_elems) + + " occupancy=" + fmt::format("{:.2f}", occupancy) + + " multiplicity=" + std::to_string(multiplicity); - if (not generate_keys(dist, h_keys_in.begin(), h_keys_in.end(), multiplicity)) { - std::cerr << "[ERROR] Invalid input distribution.\n"; - return; - } + static constexpr std::size_t max_smem_bytes = 49152; // 48 KB + static constexpr std::size_t max_capacity = max_smem_bytes / sizeof(pair_type); - // generate uniform random values - generate_keys("UNIFORM", h_values_in.begin(), h_values_in.end(), 1); - - // the size of the hash table under a given target occupancy depends on the - // number of unique keys in the input - std::size_t const unique = count_unique(h_keys_in.begin(), h_keys_in.end()); - std::size_t const capacity = std::ceil(SDIV(unique, occupancy)); + auto const elems_per_thread = num_elems / controller.get_block_size(); + auto const num_unique_keys = num_elems / multiplicity; + auto const capacity = std::ceil(num_unique_keys / occupancy); if (capacity > max_capacity) { - std::cerr << "[ERROR] Not enough shared memory available. (" << capacity * sizeof(pair_type) - << ">" << max_capacity * sizeof(pair_type) << " bytes)\n"; + std::cerr << "[ERROR] (" + full_bench_name + ") Not enough shared memory available. (" + << capacity * sizeof(pair_type) << ">" << max_capacity * sizeof(pair_type) + << " bytes)\n"; return; } - thrust::device_vector d_keys_in(h_keys_in); - thrust::device_vector d_values_in(h_values_in); + controller.benchmark(std::string{full_bench_name}, [=] __device__(cuda_benchmark::state & state) { + using map_type = typename cuco:: + static_reduction_map, Key, Value, cuda::thread_scope_block>; + using map_view_type = typename map_type::device_mutable_view; - controller.benchmark( - "static_reduction_map shared memory insert", - [=, keys_ptr = d_keys_in.data().get(), values_ptr = d_values_in.data().get()] __device__( - cuda_benchmark::state & state) { - using map_type = typename cuco::static_reduction_map, - Key, - Value, - cuda::thread_scope_block>; - using map_view_type = typename map_type::device_mutable_view; + __shared__ char sm_buffer[max_smem_bytes]; - __shared__ typename map_type::pair_atomic_type* slots; + auto g = cooperative_groups::this_thread_block(); + auto map = map_view_type::make_from_uninitialized_slots( + g, reinterpret_cast(&sm_buffer[0]), capacity, ~Key(0)); - auto g = cooperative_groups::this_thread_block(); - auto map = map_view_type::make_from_uninitialized_slots(g, slots, capacity, -1); - auto pair = pair_type(keys_ptr[g.thread_rank()], values_ptr[g.thread_rank()]); + g.sync(); - g.sync(); - - for (auto _ : state) { - map.insert(pair); - g.sync(); + for (auto _ : state) { + for (Key i = g.thread_rank(); i < num_elems; i += g.size()) { + map.insert(cuco::pair((i & (multiplicity - 1)), g.thread_rank())); } - }, - max_smem); + g.sync(); + } + state.set_operations_processed(state.max_iterations() * elems_per_thread); + }); } int main() { + int device_id{}; + cudaGetDevice(&device_id); + + cudaDeviceProp prop{}; + cudaGetDeviceProperties(&prop, device_id); + + int peak_clk{}; + cudaDeviceGetAttribute(&peak_clk, cudaDevAttrClockRate, device_id); + + // can be used to calculate throughput (ops/second) + std::cout << "GPU Clock Rate: " << std::to_string(peak_clk) << " KHz\n"; + + // start one CUDA block with 1024 threads cuda_benchmark::controller controller(1024, 1); - static_reduction_map_smem_insert_bench( - controller, 10'000, 0.8, dist_type::UNIFORM); + // unique keys; total number of keys fix; varying table occupancy + for (float occupancy = 0.5; occupancy < 1.0; occupancy += 0.1) { + static_reduction_map_smem_insert_bench( + controller, "OCCUPANCY", 10, 0, occupancy); + } + + // unique keys; total number of keys fix; varying table occupancy + for (float occupancy = 0.5; occupancy < 1.0; occupancy += 0.1) { + static_reduction_map_smem_insert_bench( + controller, "OCCUPANCY", 10, 0, occupancy); + } + + // total number of keys fix; occuoancy fix; varying key multiplicity + for (float multiplicity_log2 = 1; multiplicity_log2 < 7; ++multiplicity_log2) { + static_reduction_map_smem_insert_bench( + controller, "MULTIPLICITY", 12, multiplicity_log2, 0.8); + } + + // total number of keys fix; occuoancy fix; varying key multiplicity + for (float multiplicity_log2 = 1; multiplicity_log2 < 7; ++multiplicity_log2) { + static_reduction_map_smem_insert_bench( + controller, "MULTIPLICITY", 12, multiplicity_log2, 0.8); + } + + // occupancy fix; capacity fix; varying number of keys; varying key multiplicity + for (float i = 0; i < 7; ++i) { + static_reduction_map_smem_insert_bench( + controller, "EQUAL CAPACITY", 10 + i, 0 + i, 0.8); + } + + // occupancy fix; capacity fix; varying number of keys; varying key multiplicity + for (float i = 0; i < 7; ++i) { + static_reduction_map_smem_insert_bench( + controller, "EQUAL CAPACITY", 10 + i, 0 + i, 0.8); + } } \ No newline at end of file From ec76e8a5fb11d8d403c5747e3c6ad71005d36a59 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Mon, 9 Aug 2021 21:02:30 +0000 Subject: [PATCH 53/69] Add example for shared memory hash table. --- examples/CMakeLists.txt | 5 +- .../shared_memory_example.cu | 87 +++++++++++++++++++ .../static_reduction_map_example.cu} | 0 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 examples/static_reduction_map/shared_memory_example.cu rename examples/{static_reduction_map.cu => static_reduction_map/static_reduction_map_example.cu} (100%) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2e5967724..411de1a42 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -17,6 +17,9 @@ endfunction(ConfigureExample) ### Example sources ################################################################################## ################################################################################################### +# static_map ConfigureExample(STATIC_MAP_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/static_map/static_map_example.cu") -ConfigureExample(STATIC_REDUCTION_MAP_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/static_reduction_map.cu") +# static_reduction_map +ConfigureExample(STATIC_REDUCTION_MAP_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/static_reduction_map/static_reduction_map_example.cu") +ConfigureExample(STATIC_REDUCTION_MAP_SMEM_EXAMPLE "${CMAKE_CURRENT_SOURCE_DIR}/static_reduction_map/shared_memory_example.cu") diff --git a/examples/static_reduction_map/shared_memory_example.cu b/examples/static_reduction_map/shared_memory_example.cu new file mode 100644 index 000000000..b8c302acb --- /dev/null +++ b/examples/static_reduction_map/shared_memory_example.cu @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2021, 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 + +template +__global__ void static_reduction_map_shared_memory_kernel(OutputIt key_found) +{ + using Key = typename MapType::key_type; + using Value = typename MapType::mapped_type; + + namespace cg = cooperative_groups; + // define a mutable view for insert operations + using mutable_view_type = typename MapType::device_mutable_view; + // define a immutable view for find/contains operations + using view_type = typename MapType::device_view; + + // hash table storage in shared memory + __shared__ typename mutable_view_type::slot_type slots[Capacity]; + + // construct the table from the provided array in shared memory + auto map = mutable_view_type::make_from_uninitialized_slots( + cg::this_thread_block(), &slots[0], Capacity, -1); + + auto g = cg::this_thread_block(); + std::size_t index = threadIdx.x + blockIdx.x * blockDim.x; + int rank = g.thread_rank(); + + // insert {thread_rank, thread_rank} for each thread in thread-block + map.insert(cuco::pair(rank, rank)); + g.sync(); + + auto find_map = view_type(map); + // check if all previously inserted keys are present in the table + key_found[index] = find_map.contains(rank); +} + +/** + * @brief Demonstrates usage of the static_reduction_map in shared memory. + * + * We make use of the device-side API to construct and query a + * static reduction map in SM-local shared memory. + * + */ +int main(void) +{ + using Key = int; + using Value = int; + + // define the capacity of the map + static constexpr int capacity = 2048; + + // define the hash table typewith block-local thread scope + using map_type = + cuco::static_reduction_map, Key, Value, cuda::thread_scope_block>; + + // allocate storage for the result + thrust::device_vector result(1024, false); + + static_reduction_map_shared_memory_kernel<<<1, 1024>>>(result.begin()); + + auto success = + thrust::all_of(thrust::device, result.begin(), result.end(), thrust::identity()); + + std::cout << "Success: " << std::boolalpha << success << std::endl; +} \ No newline at end of file diff --git a/examples/static_reduction_map.cu b/examples/static_reduction_map/static_reduction_map_example.cu similarity index 100% rename from examples/static_reduction_map.cu rename to examples/static_reduction_map/static_reduction_map_example.cu From 961e88bdb1f3cf1fbd5366e79be88a5cea435ceb Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Mon, 9 Aug 2021 21:04:30 +0000 Subject: [PATCH 54/69] Fix for static_reduction_map::contains. --- include/cuco/detail/bitwise_compare.cuh | 4 +++- include/cuco/detail/static_reduction_map.inl | 2 +- include/cuco/static_reduction_map.cuh | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/include/cuco/detail/bitwise_compare.cuh b/include/cuco/detail/bitwise_compare.cuh index d554ddba3..65561e631 100644 --- a/include/cuco/detail/bitwise_compare.cuh +++ b/include/cuco/detail/bitwise_compare.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,8 @@ * limitations under the License. */ +#pragma once + #include #include diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index 7ec7a676e..0c6ff8683 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -454,7 +454,7 @@ static_reduction_map::device_view::co while (true) { auto const existing_key = current_slot->first.load(cuda::std::memory_order_relaxed); - if (detail::bitwise_compare(existing_key, empty_key_sentinel_)) { return false; } + if (detail::bitwise_compare(existing_key, this->get_empty_key_sentinel())) { return false; } if (key_equal(existing_key, k)) { return true; } diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index a09f2a25a..07a16116c 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 5931c9f56d93df5502a00f81cefef89e7b4ac28e Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Mon, 9 Aug 2021 21:30:22 +0000 Subject: [PATCH 55/69] Extend parameter range for shared memory hash table benchmark. --- benchmarks/hash_table/static_reduction_map_smem_bench.cu | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/benchmarks/hash_table/static_reduction_map_smem_bench.cu b/benchmarks/hash_table/static_reduction_map_smem_bench.cu index 147c89c92..4c18554f4 100644 --- a/benchmarks/hash_table/static_reduction_map_smem_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_smem_bench.cu @@ -54,7 +54,8 @@ void static_reduction_map_smem_insert_bench(cuda_benchmark::controller &controll std::uint32_t multiplicity_log2, float occupancy) { - using map_type = cuco::static_reduction_map, Key, Value>; + using map_type = cuco:: + static_reduction_map, Key, Value, cuda::thread_scope_block>; using pair_type = typename map_type::value_type; auto const num_elems = 1UL << num_elems_log2; @@ -145,13 +146,13 @@ int main() } // occupancy fix; capacity fix; varying number of keys; varying key multiplicity - for (float i = 0; i < 7; ++i) { + for (float i = 0; i < 11; ++i) { static_reduction_map_smem_insert_bench( controller, "EQUAL CAPACITY", 10 + i, 0 + i, 0.8); } // occupancy fix; capacity fix; varying number of keys; varying key multiplicity - for (float i = 0; i < 7; ++i) { + for (float i = 0; i < 11; ++i) { static_reduction_map_smem_insert_bench( controller, "EQUAL CAPACITY", 10 + i, 0 + i, 0.8); } From 902b93a64c3c07fbc0108bdba56b2bbc6e1f9b55 Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Mon, 9 Aug 2021 23:43:22 +0000 Subject: [PATCH 56/69] Size computation using thrust::count_if. Asynchronous bulk operations. --- include/cuco/detail/static_reduction_map.inl | 47 ++++++++----------- .../detail/static_reduction_map_kernels.cuh | 32 ++----------- include/cuco/static_reduction_map.cuh | 26 +++++----- 3 files changed, 37 insertions(+), 68 deletions(-) diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index 0c6ff8683..a44544755 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -39,8 +39,7 @@ static_reduction_map::static_reductio empty_key_sentinel_{empty_key_sentinel}, empty_value_sentinel_{ReductionOp::identity}, op_{reduction_op}, - slot_allocator_{alloc}, - counter_allocator_{alloc} + slot_allocator_{alloc} { slots_ = std::allocator_traits::allocate(slot_allocator_, capacity_); @@ -79,29 +78,8 @@ void static_reduction_map::insert( auto const grid_size = (tile_size * num_keys + stride * block_size - 1) / (stride * block_size); auto view = get_device_mutable_view(); - atomic_ctr_type *h_num_successes, *d_num_successes; - CUCO_CUDA_TRY(cudaMallocHost(&h_num_successes, sizeof(atomic_ctr_type))); - - auto tmp_counter_allocator = counter_allocator_; - d_num_successes = - std::allocator_traits::allocate(tmp_counter_allocator, 1); - - h_num_successes->store(static_cast(0), cuda::std::memory_order_relaxed); - CUCO_CUDA_TRY(cudaMemcpyAsync( - d_num_successes, h_num_successes, sizeof(atomic_ctr_type), cudaMemcpyHostToDevice, stream)); - - detail::insert<<>>( - first, first + num_keys, d_num_successes, view, hash, key_equal); - - CUCO_CUDA_TRY(cudaMemcpyAsync( - h_num_successes, d_num_successes, sizeof(atomic_ctr_type), cudaMemcpyDeviceToHost, stream)); - CUCO_CUDA_TRY(cudaStreamSynchronize(stream)); - - size_ += h_num_successes->load(cuda::std::memory_order_relaxed); - - CUCO_CUDA_TRY(cudaFreeHost(h_num_successes)); - std::allocator_traits::deallocate( - tmp_counter_allocator, d_num_successes, 1); + detail::insert + <<>>(first, first + num_keys, view, hash, key_equal); } template ::find(Input detail::find <<>>(first, last, output_begin, view, hash, key_equal); - CUCO_CUDA_TRY(cudaStreamSynchronize(stream)); } namespace detail { @@ -196,7 +173,23 @@ void static_reduction_map::contains( detail::contains <<>>(first, last, output_begin, view, hash, key_equal); - CUCO_CUDA_TRY(cudaStreamSynchronize(stream)); +} + +template +std::size_t static_reduction_map::get_size( + cudaStream_t stream) const noexcept +{ + // Convert pair_type to thrust::tuple to allow assigning to a zip iterator + auto begin = + thrust::make_transform_iterator(raw_slots_begin(), detail::slot_to_tuple{}); + auto end = begin + get_capacity(); + auto filled = detail::slot_is_filled{get_empty_key_sentinel()}; + + return thrust::count_if(thrust::cuda::par.on(stream), begin, end, filled); } template -__global__ void insert( - InputIt first, InputIt last, atomicT* num_successes, viewT view, Hash hash, KeyEqual key_equal) +__global__ void insert(InputIt first, InputIt last, viewT view, Hash hash, KeyEqual key_equal) { - typedef cub::BlockReduce BlockReduce; - __shared__ typename BlockReduce::TempStorage temp_storage; - std::size_t thread_num_successes = 0; - auto tid = block_size * blockIdx.x + threadIdx.x; auto it = first + tid; while (it < last) { typename viewT::value_type const insert_pair{*it}; - if (view.insert(insert_pair, hash, key_equal)) { thread_num_successes++; } + view.insert(insert_pair, hash, key_equal); it += gridDim.x * block_size; } - - // compute number of successfully inserted elements for each block - // and atomically add to the grand total - std::size_t block_num_successes = BlockReduce(temp_storage).Sum(thread_num_successes); - if (threadIdx.x == 0) { *num_successes += block_num_successes; } } /** @@ -126,17 +115,11 @@ __global__ void insert( template -__global__ void insert( - InputIt first, InputIt last, atomicT* num_successes, viewT view, Hash hash, KeyEqual key_equal) +__global__ void insert(InputIt first, InputIt last, viewT view, Hash hash, KeyEqual key_equal) { - typedef cub::BlockReduce BlockReduce; - __shared__ typename BlockReduce::TempStorage temp_storage; - std::size_t thread_num_successes = 0; - auto tile = cg::tiled_partition(cg::this_thread_block()); auto tid = block_size * blockIdx.x + threadIdx.x; auto it = first + tid / tile_size; @@ -147,16 +130,9 @@ __global__ void insert( static_cast(thrust::get<0>(*it)), static_cast(thrust::get<1>(*it))}; - if (view.insert(tile, insert_pair, hash, key_equal) && tile.thread_rank() == 0) { - thread_num_successes++; - } + view.insert(tile, insert_pair, hash, key_equal); it += (gridDim.x * block_size) / tile_size; } - - // compute number of successfully inserted elements for each block - // and atomically add to the grand total - std::size_t block_num_successes = BlockReduce(temp_storage).Sum(thread_num_successes); - if (threadIdx.x == 0) { *num_successes += block_num_successes; } } /** diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 07a16116c..dfb44a9a7 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -271,12 +271,9 @@ class static_reduction_map { using atomic_mapped_type = cuda::atomic; using pair_atomic_type = cuco::pair_type; using slot_type = pair_atomic_type; - using atomic_ctr_type = cuda::atomic; using allocator_type = Allocator; using slot_allocator_type = typename std::allocator_traits::rebind_alloc; - using counter_allocator_type = - typename std::allocator_traits::rebind_alloc; #if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 700) static_assert(atomic_key_type::is_always_lock_free, @@ -1101,16 +1098,21 @@ class static_reduction_map { /** * @brief Gets the number of elements in the hash map. * + * @param stream CUDA stream this operation is issued in (synchronizes with host) * @return The number of elements in the map */ - std::size_t get_size() const noexcept { return size_; } + std::size_t get_size(cudaStream_t stream = 0) const noexcept; /** * @brief Gets the load factor of the hash map. * + * @param stream CUDA stream this operation is issued in (synchronizes with host) * @return The load factor of the hash map */ - float get_load_factor() const noexcept { return static_cast(size_) / capacity_; } + float get_load_factor(cudaStream_t stream = 0) const noexcept + { + return static_cast(get_size(stream)) / capacity_; + } /** * @brief Gets the sentinel value used to represent an empty key slot. @@ -1162,14 +1164,12 @@ class static_reduction_map { value_type const* raw_slots_end() const noexcept { return raw_slots_begin() + get_capacity(); } - pair_atomic_type* slots_{nullptr}; ///< Pointer to flat slots storage - std::size_t capacity_{}; ///< Total number of slots - std::size_t size_{}; ///< Number of keys in map - Key empty_key_sentinel_{}; ///< Key value that represents an empty slot - Value empty_value_sentinel_{}; ///< Initial value of empty slot - ReductionOp op_{}; ///< Binary operation reduction function object - slot_allocator_type slot_allocator_{}; ///< Allocator used to allocate slots - counter_allocator_type counter_allocator_{}; ///< Allocator used to allocate counters + pair_atomic_type* slots_{nullptr}; ///< Pointer to flat slots storage + std::size_t capacity_{}; ///< Total number of slots + Key empty_key_sentinel_{}; ///< Key value that represents an empty slot + Value empty_value_sentinel_{}; ///< Initial value of empty slot + ReductionOp op_{}; ///< Binary operation reduction function object + slot_allocator_type slot_allocator_{}; ///< Allocator used to allocate slots }; } // namespace cuco From d440243842e0858f7d8f4ea0a094144e4f28a7ca Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Mon, 9 Aug 2021 18:21:15 -0700 Subject: [PATCH 57/69] Add throughput column to nvbench benchmarks. --- benchmarks/hash_table/static_reduction_map_bench.cu | 2 ++ benchmarks/hash_table/static_reduction_map_param_grid_search.cu | 2 ++ benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu | 2 ++ benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu | 2 ++ benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu | 2 ++ 5 files changed, 10 insertions(+) diff --git a/benchmarks/hash_table/static_reduction_map_bench.cu b/benchmarks/hash_table/static_reduction_map_bench.cu index 863477a7c..2de5fa585 100644 --- a/benchmarks/hash_table/static_reduction_map_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_bench.cu @@ -117,6 +117,8 @@ void nvbench_cuco_static_reduction_map_insert( thrust::make_zip_iterator(thrust::make_tuple(d_keys_in.begin(), d_values_in.begin())); auto d_pairs_in_end = d_pairs_in_begin + num_elems; + state.add_element_count(num_elems); + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { map_type map{capacity, -1}; diff --git a/benchmarks/hash_table/static_reduction_map_param_grid_search.cu b/benchmarks/hash_table/static_reduction_map_param_grid_search.cu index 27bcbf38d..4063d7c73 100644 --- a/benchmarks/hash_table/static_reduction_map_param_grid_search.cu +++ b/benchmarks/hash_table/static_reduction_map_param_grid_search.cu @@ -70,6 +70,8 @@ void nvbench_cuco_static_reduction_map_custom_op_backoff_delay( thrust::make_zip_iterator(thrust::make_tuple(d_keys.begin(), d_values.begin())); auto d_pairs_end = d_pairs_begin + num_elems; + state.add_element_count(num_elems); + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { map_type map{capacity, -1}; diff --git a/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu index efbe7799d..5bf48e4af 100644 --- a/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu +++ b/benchmarks/reduce_by_key/cub_reduce_by_key_bench.cu @@ -72,6 +72,8 @@ void nvbench_cub_reduce_by_key(nvbench::state& state, nvbench::type_list d_temp(std::max(temp_bytes_sort, temp_bytes_reduce)); + state.add_element_count(num_elems_in); + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { timer.start(); diff --git a/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu index 96d5ee2b3..3c23330b9 100644 --- a/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu +++ b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu @@ -105,6 +105,8 @@ void nvbench_cuco_static_reduction_map_reduce_by_key( thrust::make_zip_iterator(thrust::make_tuple(d_keys.begin(), d_values.begin())); auto d_pairs_end = d_pairs_begin + num_elems; + state.add_element_count(num_elems); + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { map_type map{capacity, -1}; diff --git a/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu index 8cc5ef3cc..4069c3b1d 100644 --- a/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu +++ b/benchmarks/reduce_by_key/thrust_reduce_by_key_bench.cu @@ -69,6 +69,8 @@ void nvbench_thrust_reduce_by_key(nvbench::state& state, nvbench::type_list d_keys(h_keys); thrust::device_vector d_values(h_values); + state.add_element_count(num_elems); + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { timer.start(); From 4339b2bd84c851b33845174bd174957377cb9d8e Mon Sep 17 00:00:00 2001 From: Daniel Juenger Date: Tue, 24 Aug 2021 23:28:04 +0000 Subject: [PATCH 58/69] Fix for reductions over FP types. --- include/cuco/detail/reduction_ops.cuh | 229 ++++++++++++++++++ include/cuco/static_reduction_map.cuh | 111 +-------- .../static_reduction_map_test.cu | 8 +- 3 files changed, 236 insertions(+), 112 deletions(-) create mode 100644 include/cuco/detail/reduction_ops.cuh diff --git a/include/cuco/detail/reduction_ops.cuh b/include/cuco/detail/reduction_ops.cuh new file mode 100644 index 000000000..c8496470e --- /dev/null +++ b/include/cuco/detail/reduction_ops.cuh @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2021, 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 + +namespace cuco { + +/** + * @brief `+` reduction functor that internally uses an atomic fetch-and-add + * operation. + * + * @tparam T The data type used for reduction + */ +template +struct reduce_add { + using value_type = T; + static constexpr T identity = 0; + + template + __device__ T apply(cuda::atomic& slot, T2 const& value) const + { + return slot.fetch_add(value, cuda::memory_order_relaxed); + } +}; + +// remove this workaround once libcu++ extends FP atomics support +// https://github.com/NVIDIA/libcudacxx/issues/104 +template <> +struct reduce_add { + using value_type = float; + static constexpr float identity = 0; + + template + __device__ float apply(cuda::atomic& slot, T2 const& value) const + { + return atomicAdd(reinterpret_cast(&slot), value); + } +}; + +template <> +struct reduce_add { + using value_type = double; + static constexpr double identity = 0; + + template + __device__ double apply(cuda::atomic& slot, T2 const& value) const + { + return atomicAdd(reinterpret_cast(&slot), value); + } +}; + +/** + * @brief `-` reduction functor that internally uses an atomic fetch-and-add + * operation. + * + * @tparam T The data type used for reduction + */ +template +struct reduce_sub { + using value_type = T; + static constexpr T identity = 0; + + template + __device__ T apply(cuda::atomic& slot, T2 const& value) const + { + return slot.fetch_sub(value, cuda::memory_order_relaxed); + } +}; + +template <> +struct reduce_sub { + using value_type = float; + static constexpr float identity = 0; + + template + __device__ float apply(cuda::atomic& slot, T2 const& value) const + { + return atomicSub(reinterpret_cast(&slot), value); + } +}; + +template <> +struct reduce_sub { + using value_type = double; + static constexpr double identity = 0; + + template + __device__ double apply(cuda::atomic& slot, T2 const& value) const + { + return atomicSub(reinterpret_cast(&slot), value); + } +}; + +/** + * @brief `min` reduction functor that internally uses an atomic fetch-and-add + * operation. + * + * @tparam T The data type used for reduction + */ +template +struct reduce_min { + using value_type = T; + static constexpr T identity = std::numeric_limits::max(); + + template + __device__ T apply(cuda::atomic& slot, T2 const& value) const + { + return slot.fetch_min(value, cuda::memory_order_relaxed); + } +}; + +template <> +struct reduce_min { + using value_type = float; + static constexpr float identity = std::numeric_limits::max(); + + template + __device__ float apply(cuda::atomic& slot, T2 const& value) const + { + return atomicMin(reinterpret_cast(&slot), value); + } +}; + +template <> +struct reduce_min { + using value_type = double; + static constexpr double identity = std::numeric_limits::max(); + + template + __device__ double apply(cuda::atomic& slot, T2 const& value) const + { + return atomicMin(reinterpret_cast(&slot), value); + } +}; + +/** + * @brief `max` reduction functor that internally uses an atomic fetch-and-add + * operation. + * + * @tparam T The data type used for reduction + */ +template +struct reduce_max { + using value_type = T; + static constexpr T identity = std::numeric_limits::lowest(); + + template + __device__ T apply(cuda::atomic& slot, T2 const& value) const + { + return slot.fetch_max(value, cuda::memory_order_relaxed); + } +}; + +template <> +struct reduce_max { + using value_type = float; + static constexpr float identity = std::numeric_limits::lowest(); + + template + __device__ float apply(cuda::atomic& slot, T2 const& value) const + { + return atomicMax(reinterpret_cast(&slot), value); + } +}; + +template <> +struct reduce_max { + using value_type = double; + static constexpr double identity = std::numeric_limits::lowest(); + + template + __device__ double apply(cuda::atomic& slot, T2 const& value) const + { + return atomicMax(reinterpret_cast(&slot), value); + } +}; + +/** + * @brief Wrapper for a user-defined custom reduction operator. + * @brief Internally uses an atomic compare-and-swap loop. + * + * @tparam T The data type used for reduction + * @tparam Identity Neutral element under the given reduction group + * @tparam Op Commutative and associative binary operator + */ +template +struct custom_op { + using value_type = T; + static constexpr T identity = Identity; + + Op op; + + template + __device__ T apply(cuda::atomic& slot, T2 const& value) const + { + [[maybe_unused]] unsigned ns = BackoffBaseDelay; + + auto old = slot.load(cuda::memory_order_relaxed); + while (not slot.compare_exchange_strong(old, op(old, value), cuda::memory_order_relaxed)) { +#if __CUDA_ARCH__ >= 700 + // exponential backoff strategy to reduce atomic contention + if (true) { + asm volatile("nanosleep.u32 %0;" ::"r"((unsigned)ns) :); + if (ns < BackoffMaxDelay) { ns *= 2; } + } +#endif + } + return old; + } +}; + +} // namespace cuco \ No newline at end of file diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index dfb44a9a7..3cc679aff 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -42,121 +42,12 @@ #include #include #include +#include #include #include namespace cuco { -/** - * @brief `+` reduction functor that internally uses an atomic fetch-and-add - * operation. - * - * @tparam T The data type used for reduction - */ -template -struct reduce_add { - using value_type = T; - static constexpr T identity = 0; - - template - __device__ T apply(cuda::atomic& slot, T2 const& value) const - { - return slot.fetch_add(value, cuda::memory_order_relaxed); - } -}; - -/** - * @brief `-` reduction functor that internally uses an atomic fetch-and-add - * operation. - * - * @tparam T The data type used for reduction - */ -template -struct reduce_sub { - using value_type = T; - static constexpr T identity = 0; - - template - __device__ T apply(cuda::atomic& slot, T2 const& value) const - { - return slot.fetch_sub(value, cuda::memory_order_relaxed); - } -}; - -/** - * @brief `min` reduction functor that internally uses an atomic fetch-and-add - * operation. - * - * @tparam T The data type used for reduction - */ -template -struct reduce_min { - using value_type = T; - static constexpr T identity = std::numeric_limits::max(); - - template - __device__ T apply(cuda::atomic& slot, T2 const& value) const - { - return slot.fetch_min(value, cuda::memory_order_relaxed); - } -}; - -/** - * @brief `max` reduction functor that internally uses an atomic fetch-and-add - * operation. - * - * @tparam T The data type used for reduction - */ -template -struct reduce_max { - using value_type = T; - static constexpr T identity = std::numeric_limits::lowest(); - - template - __device__ T apply(cuda::atomic& slot, T2 const& value) const - { - return slot.fetch_max(value, cuda::memory_order_relaxed); - } -}; - -/** - * @brief Wrapper for a user-defined custom reduction operator. - * @brief Internally uses an atomic compare-and-swap loop. - * - * @tparam T The data type used for reduction - * @tparam Identity Neutral element under the given reduction group - * @tparam Op Commutative and associative binary operator - */ -template -struct custom_op { - using value_type = T; - static constexpr T identity = Identity; - - Op op; - - template - __device__ T apply(cuda::atomic& slot, T2 const& value) const - { - [[maybe_unused]] unsigned ns = BackoffBaseDelay; - - auto old = slot.load(cuda::memory_order_relaxed); - while (not slot.compare_exchange_strong(old, op(old, value), cuda::memory_order_relaxed)) { -#if __CUDA_ARCH__ >= 700 - // exponential backoff strategy to reduce atomic contention - if (true) { - asm volatile("nanosleep.u32 %0;" ::"r"((unsigned)ns) :); - if (ns < BackoffMaxDelay) { ns *= 2; } - } -#endif - } - return old; - } -}; - /** * @brief A GPU-accelerated, unordered, associative container of key-value * pairs that reduces the values associated to the same key according to a diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index 51fee5740..f1540e041 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -31,7 +31,9 @@ TEMPLATE_TEST_CASE_SIG("Insert all identical keys", "", ((typename Key, typename Value, typename Op), Key, Value, Op), (int32_t, int32_t, cuco::reduce_add), - (int32_t, int32_t, custom_reduce_add)) + (int32_t, int32_t, custom_reduce_add), + (int32_t, float, cuco::reduce_add), + (int64_t, double, cuco::reduce_add)) { thrust::device_vector keys(100, 42); thrust::device_vector values(keys.size(), 1); @@ -133,7 +135,9 @@ TEMPLATE_TEST_CASE_SIG("Shared memory hast table.", "", ((typename Key, typename Value, typename Op), Key, Value, Op), (int32_t, int32_t, cuco::reduce_add), - (int32_t, int32_t, custom_reduce_add)) + (int32_t, int32_t, custom_reduce_add), + (int32_t, float, cuco::reduce_add), + (int64_t, double, cuco::reduce_add)) { constexpr std::size_t N = 256; thrust::device_vector key_found(N, false); From 6293117a6ea406319cbb325fabe690473425507c Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Wed, 13 Oct 2021 16:43:19 -0500 Subject: [PATCH 59/69] Add support for both static/dynamic extent of device_view. --- .../hash_table/static_reduction_map_bench.cu | 144 +++++++++++++-- .../static_reduction_map_smem_bench.cu | 8 +- .../shared_memory_example.cu | 5 +- include/cuco/detail/static_reduction_map.inl | 32 ++-- include/cuco/static_reduction_map.cuh | 164 ++++++++++++------ .../static_reduction_map_test.cu | 8 +- 6 files changed, 275 insertions(+), 86 deletions(-) diff --git a/benchmarks/hash_table/static_reduction_map_bench.cu b/benchmarks/hash_table/static_reduction_map_bench.cu index 2de5fa585..917200290 100644 --- a/benchmarks/hash_table/static_reduction_map_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_bench.cu @@ -20,6 +20,9 @@ #include #include #include +#include + +namespace cg = cooperative_groups; /** * @brief Enum representation for reduction operators @@ -77,9 +80,134 @@ struct op_type_map { using type = cuco::custom_op, 0>; }; -/** - * @brief A benchmark evaluating insert performance. - */ +enum class Extent { + DYNAMIC, STATIC +}; + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + // Enum type: + Extent, + // Callable to generate input strings: + // Short identifier used for tables, command-line args, etc. + // Used when context is available to figure out the enum type. + [](Extent e) { + switch (e) { + case Extent::DYNAMIC: return "DYNAMIC"; + case Extent::STATIC: return "STATIC"; + default: return "ERROR"; + } + }, + // Callable to generate descriptions: + // If non-empty, these are used in `--list` to describe values. + // Used when context may not be available to figure out the type from the + // input string. + // Just use `[](auto) { return std::string{}; }` if you don't want these. + [](auto) { return std::string{}; }) + +struct always_false{ + always_false() = default; + + __host__ __device__ + operator bool(){ + return b; + } + +private: + bool b{false}; +}; + +template +__global__ +void dynamic_shmem_insert_kernel(std::size_t num_keys, std::size_t multiplicity, std::size_t capacity, always_false pred, bool* do_not_use){ + + using Map = typename cuco::static_reduction_map::device_mutable_view; + + #pragma diag_suppress static_var_with_dynamic_init + extern __shared__ typename Map::slot_type slots[]; + + auto map = Map::make_from_uninitialized_slots(cg::this_thread_block(), slots, capacity, -1); + auto tid = threadIdx.x + blockIdx.x * blockDim.x; + + bool result; + while(tid < num_keys){ + for(int i = 0; i < multiplicity; ++i){ + result = map.insert(cuco::pair{tid, i}); + } + tid += blockDim.x; + } + + // Placeholder predicated store to inject artificial side-effects and keep compiler from discarding + // the code above + if(pred){ + *do_not_use = result; + } +} + +template +__global__ +void static_shmem_insert_kernel(std::size_t num_keys, std::size_t multiplicity, always_false pred, bool* do_not_use){ + + using Map = typename cuco::static_reduction_map::device_mutable_view; + + #pragma diag_suppress static_var_with_dynamic_init + __shared__ typename Map::slot_type slots[Capacity]; + + auto map = Map::make_from_uninitialized_slots(cg::this_thread_block(), slots, -1); + auto tid = threadIdx.x + blockIdx.x * blockDim.x; + + bool result; + while(tid < num_keys){ + for(int i = 0; i < multiplicity; ++i){ + result = map.insert(cuco::pair{tid, i}); + } + tid += blockDim.x; + } + + // Placeholder predicated store to inject artificial side-effects and keep compiler from discarding + // the code above +} + +template +void static_shmem(nvbench::state& state, + nvbench::type_list, nvbench::enum_type, nvbench::enum_type>) +{ + using OpType = typename op_type_map::type; + + auto const occupancy = state.get_float64("Occupancy"); + auto const num_keys = static_cast(std::floor(Capacity * occupancy)); + auto const multiplicity = 1; + + if(num_keys > Capacity){ + throw; + } + + state.exec([&](nvbench::launch& launch){ + if constexpr(E == Extent::STATIC){ + static_shmem_insert_kernel<<<512, 1024, 0, launch.get_stream()>>>(num_keys, multiplicity, always_false{}, (bool*)nullptr); + } else { + using slot_type = typename cuco::static_reduction_map::device_mutable_view<>::slot_type; + dynamic_shmem_insert_kernel<<<512, 1024, Capacity * sizeof(slot_type), launch.get_stream()>>>(num_keys, multiplicity, Capacity, always_false{}, (bool*)nullptr); + } + }); +} + + + + +// type parameter dimensions for benchmark +using key_type_range = nvbench::type_list; +using value_type_range = nvbench::type_list; +using op_type_range = nvbench::enum_type_list; +using capacity_range = nvbench::enum_type_list<6000>; +using extent_options = nvbench::enum_type_list; + +NVBENCH_BENCH_TYPES(static_shmem, + NVBENCH_TYPE_AXES(key_type_range, value_type_range, op_type_range, capacity_range, extent_options)) + .set_name("Insert Static vs Dynamic Extent") + .set_type_axes_names({"Key", "Value", "ReductionOp", "Capacity", "Extent"}) + .add_float64_axis("Occupancy", nvbench::range(0.5, 0.9, 0.1)); + + template void nvbench_cuco_static_reduction_map_insert( nvbench::state& state, nvbench::type_list>) @@ -113,8 +241,7 @@ void nvbench_cuco_static_reduction_map_insert( thrust::device_vector d_keys_in(h_keys_in); thrust::device_vector d_values_in(h_values_in); - auto d_pairs_in_begin = - thrust::make_zip_iterator(thrust::make_tuple(d_keys_in.begin(), d_values_in.begin())); + auto d_pairs_in_begin = thrust::make_zip_iterator(thrust::make_tuple(d_keys_in.begin(), d_values_in.begin())); auto d_pairs_in_end = d_pairs_in_begin + num_elems; state.add_element_count(num_elems); @@ -129,11 +256,6 @@ void nvbench_cuco_static_reduction_map_insert( }); } -// type parameter dimensions for benchmark -using key_type_range = nvbench::type_list; -using value_type_range = nvbench::type_list; -using op_type_range = - nvbench::enum_type_list; // benchmark setups @@ -169,4 +291,4 @@ NVBENCH_BENCH_TYPES(nvbench_cuco_static_reduction_map_insert, .add_float64_axis("Occupancy", nvbench::range(0.5, 0.9, 0.1)) .add_int64_axis("Multiplicity", {1, 10, 100, 1'000, 10'000, 100'000, 1'000'000}) // key multiplicity range - .add_string_axis("Distribution", {"UNIFORM"}); \ No newline at end of file + .add_string_axis("Distribution", {"UNIFORM"}); diff --git a/benchmarks/hash_table/static_reduction_map_smem_bench.cu b/benchmarks/hash_table/static_reduction_map_smem_bench.cu index 4c18554f4..3876b016d 100644 --- a/benchmarks/hash_table/static_reduction_map_smem_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_smem_bench.cu @@ -82,15 +82,13 @@ void static_reduction_map_smem_insert_bench(cuda_benchmark::controller &controll } controller.benchmark(std::string{full_bench_name}, [=] __device__(cuda_benchmark::state & state) { - using map_type = typename cuco:: - static_reduction_map, Key, Value, cuda::thread_scope_block>; - using map_view_type = typename map_type::device_mutable_view; + using map_type = typename cuco::static_reduction_map, Key, Value, cuda::thread_scope_block>; + using map_view_type = typename map_type::device_mutable_view<>; __shared__ char sm_buffer[max_smem_bytes]; auto g = cooperative_groups::this_thread_block(); - auto map = map_view_type::make_from_uninitialized_slots( - g, reinterpret_cast(&sm_buffer[0]), capacity, ~Key(0)); + auto map = map_view_type::make_from_uninitialized_slots(g, reinterpret_cast(&sm_buffer[0]), capacity, ~Key(0)); g.sync(); diff --git a/examples/static_reduction_map/shared_memory_example.cu b/examples/static_reduction_map/shared_memory_example.cu index b8c302acb..e57cd75e4 100644 --- a/examples/static_reduction_map/shared_memory_example.cu +++ b/examples/static_reduction_map/shared_memory_example.cu @@ -32,11 +32,12 @@ __global__ void static_reduction_map_shared_memory_kernel(OutputIt key_found) namespace cg = cooperative_groups; // define a mutable view for insert operations - using mutable_view_type = typename MapType::device_mutable_view; + using mutable_view_type = typename MapType::device_mutable_view<>; // define a immutable view for find/contains operations - using view_type = typename MapType::device_view; + using view_type = typename MapType::device_view<>; // hash table storage in shared memory + #pragma diag_suppress static_var_with_dynamic_init __shared__ typename mutable_view_type::slot_type slots[Capacity]; // construct the table from the provided array in shared memory diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index a44544755..eb0a211b4 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -197,9 +197,10 @@ template +template template __device__ bool -static_reduction_map::device_mutable_view::insert( +static_reduction_map::device_mutable_view::insert( value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept { auto current_slot{initial_slot(insert_pair.first, hash)}; @@ -232,9 +233,10 @@ template +template template __device__ bool -static_reduction_map::device_mutable_view::insert( +static_reduction_map::device_mutable_view::insert( CG const& g, value_type const& insert_pair, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(g, insert_pair.first, hash); @@ -299,10 +301,11 @@ template +template template __device__ - typename static_reduction_map::device_view::iterator - static_reduction_map::device_view::find( + typename static_reduction_map::device_view::iterator + static_reduction_map::device_view::find( Key const& k, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(k, hash); @@ -326,10 +329,11 @@ template +template template -__device__ typename static_reduction_map::device_view:: +__device__ typename static_reduction_map::device_view:: const_iterator - static_reduction_map::device_view::find( + static_reduction_map::device_view::find( Key const& k, Hash hash, KeyEqual key_equal) const noexcept { auto current_slot = initial_slot(k, hash); @@ -353,10 +357,11 @@ template +template template __device__ - typename static_reduction_map::device_view::iterator - static_reduction_map::device_view::find( + typename static_reduction_map::device_view::iterator + static_reduction_map::device_view::find( CG const& g, Key const& k, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(g, k, hash); @@ -394,10 +399,11 @@ template +template template -__device__ typename static_reduction_map::device_view:: +__device__ typename static_reduction_map::device_view:: const_iterator - static_reduction_map::device_view::find( + static_reduction_map::device_view::find( CG const& g, Key const& k, Hash hash, KeyEqual key_equal) const noexcept { auto current_slot = initial_slot(g, k, hash); @@ -437,9 +443,10 @@ template +template template __device__ bool -static_reduction_map::device_view::contains( +static_reduction_map::device_view::contains( Key const& k, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(k, hash); @@ -460,9 +467,10 @@ template +template template __device__ bool -static_reduction_map::device_view::contains( +static_reduction_map::device_view::contains( CG const& g, Key const& k, Hash hash, KeyEqual key_equal) noexcept { auto current_slot = initial_slot(g, k, hash); diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 3cc679aff..e12477036 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -48,6 +48,8 @@ namespace cuco { + static constexpr std::size_t dynamic_extent = std::numeric_limits::max(); + /** * @brief A GPU-accelerated, unordered, associative container of key-value * pairs that reduces the values associated to the same key according to a @@ -313,6 +315,26 @@ class static_reduction_map { KeyEqual key_equal = KeyEqual{}); private: + + + + template + struct slot_storage{ + slot_storage() = delete; + constexpr explicit slot_storage(Slot* p, std::size_t) noexcept : ptr{p} {} + Slot* ptr; + static constexpr std::size_t size = Extent; + }; + + template + struct slot_storage{ + slot_storage() = delete; + constexpr slot_storage(Slot* p, std::size_t n) noexcept : ptr{p}, size{n} {} + Slot* ptr; + std::size_t size; + }; + + template class device_view_base { protected: // Import member type definitions from `static_reduction_map` @@ -323,9 +345,10 @@ class static_reduction_map { using const_iterator = pair_atomic_type const*; using slot_type = slot_type; + static constexpr std::size_t extent = Extent; + private: - pair_atomic_type* slots_{}; ///< Pointer to flat slots storage - std::size_t capacity_{}; ///< Total number of slots + slot_storage storage_; Key empty_key_sentinel_{}; ///< Key value that represents an empty slot Value empty_value_sentinel_{}; ///< Initial Value of empty slot ReductionOp op_{}; ///< Binary operation reduction function object @@ -335,14 +358,23 @@ class static_reduction_map { std::size_t capacity, Key empty_key_sentinel, ReductionOp reduction_op) noexcept - : slots_{slots}, - capacity_{capacity}, + : storage_{slots, capacity}, empty_key_sentinel_{empty_key_sentinel}, empty_value_sentinel_{ReductionOp::identity}, op_{reduction_op} { + assert(extent == dynamic_extent or capacity == extent); } + + template* = nullptr> + __host__ __device__ + constexpr device_view_base(slot_type (&arr)[N], Key empty_key_sentinel, ReductionOp op) + : storage_{arr, N}, empty_key_sentinel_{empty_key_sentinel}, + empty_value_sentinel_{ReductionOp::identity}, op_{op} {} + + /** * @brief Returns the initial slot for a given key `k` * @@ -352,9 +384,10 @@ class static_reduction_map { * @return Pointer to the initial slot for `k` */ template - __device__ iterator initial_slot(Key const& k, Hash hash) noexcept + __device__ + constexpr iterator initial_slot(Key const& k, Hash hash) noexcept { - return &slots_[hash(k) % capacity_]; + return begin_slot() + (hash(k) % get_capacity()); } /** @@ -366,9 +399,10 @@ class static_reduction_map { * @return Pointer to the initial slot for `k` */ template - __device__ const_iterator initial_slot(Key const& k, Hash hash) const noexcept + __device__ + constexpr const_iterator initial_slot(Key const& k, Hash hash) const noexcept { - return &slots_[hash(k) % capacity_]; + return begin_slot() + (hash(k) % get_capacity()); } /** @@ -384,9 +418,10 @@ class static_reduction_map { * @return Pointer to the initial slot for `k` */ template - __device__ iterator initial_slot(CG const& g, Key const& k, Hash hash) noexcept + __device__ + constexpr iterator initial_slot(CG const& g, Key const& k, Hash hash) noexcept { - return &slots_[(hash(k) + g.thread_rank()) % capacity_]; + return begin_slot() + (hash(k) + g.thread_rank()) % get_capacity(); } /** @@ -404,7 +439,7 @@ class static_reduction_map { template __device__ const_iterator initial_slot(CG const& g, Key const& k, Hash hash) const noexcept { - return &slots_[(hash(k) + g.thread_rank()) % capacity_]; + return begin_slot() + (hash(k) + g.thread_rank()) % get_capacity(); } /** @@ -415,7 +450,9 @@ class static_reduction_map { * @param s The slot to advance * @return The next slot after `s` */ - __device__ iterator next_slot(iterator s) noexcept { return (++s < end()) ? s : begin_slot(); } + __device__ iterator next_slot(iterator s) noexcept { + return (++s < end()) ? s : begin_slot(); + } /** * @brief Given a slot `s`, returns the next slot. @@ -444,8 +481,8 @@ class static_reduction_map { template __device__ iterator next_slot(CG const& g, iterator s) noexcept { - uint32_t index = s - slots_; - return &slots_[(index + g.size()) % capacity_]; + auto const index = thrust::distance(begin_slot(), s); + return begin_slot() + (index + g.size()) % get_capacity(); } /** @@ -462,8 +499,8 @@ class static_reduction_map { template __device__ const_iterator next_slot(CG const& g, const_iterator s) const noexcept { - uint32_t index = s - slots_; - return &slots_[(index + g.size()) % capacity_]; + auto const index = thrust::distance(begin_slot(), s); + return begin_slot() + (index + g.size()) % get_capacity(); } /** @@ -481,8 +518,7 @@ class static_reduction_map { */ template - __device__ static void initialize_slots( - CG g, pair_atomic_type* slots, std::size_t num_slots, Key k, Value v) + __device__ static void initialize_slots(CG g, slot_type* slots, std::size_t num_slots, Key k, Value v) { auto tid = g.thread_rank(); while (tid < num_slots) { @@ -505,21 +541,24 @@ class static_reduction_map { * * @return Slots array */ - __device__ pair_atomic_type* get_slots() noexcept { return slots_; } + __device__ + constexpr slot_type* get_slots() noexcept { return storage_.ptr; } /** * @brief Gets slots array. * * @return Slots array */ - __device__ pair_atomic_type const* get_slots() const noexcept { return slots_; } + __device__ + constexpr slot_type const* get_slots() const noexcept { return storage_.ptr; } /** * @brief Gets the maximum number of elements the hash map can hold. * * @return The maximum number of elements the hash map can hold */ - __host__ __device__ std::size_t get_capacity() const noexcept { return capacity_; } + __host__ __device__ + constexpr std::size_t get_capacity() const noexcept { return storage_.size; } /** * @brief Gets the sentinel value used to represent an empty key slot. @@ -551,7 +590,8 @@ class static_reduction_map { * * @return Iterator to the first slot */ - __device__ iterator begin_slot() noexcept { return slots_; } + __device__ + constexpr iterator begin_slot() noexcept { return get_slots(); } /** * @brief Returns iterator to the first slot. @@ -566,21 +606,24 @@ class static_reduction_map { * * @return Iterator to the first slot */ - __device__ const_iterator begin_slot() const noexcept { return slots_; } + __device__ + constexpr const_iterator begin_slot() const noexcept { return get_slots(); } /** * @brief Returns a const_iterator to one past the last slot. * * @return A const_iterator to one past the last slot */ - __host__ __device__ const_iterator end_slot() const noexcept { return slots_ + capacity_; } + __host__ __device__ + constexpr const_iterator end_slot() const noexcept { return begin_slot() + get_capacity(); } /** * @brief Returns an iterator to one past the last slot. * * @return An iterator to one past the last slot */ - __host__ __device__ iterator end_slot() noexcept { return slots_ + capacity_; } + __host__ __device__ + constexpr iterator end_slot() noexcept { return begin_slot() + get_capacity(); } /** * @brief Returns a const_iterator to one past the last slot. @@ -590,7 +633,8 @@ class static_reduction_map { * * @return A const_iterator to one past the last slot */ - __host__ __device__ const_iterator end() const noexcept { return end_slot(); } + __host__ __device__ + constexpr const_iterator end() const noexcept { return end_slot(); } /** * @brief Returns an iterator to one past the last slot. @@ -600,7 +644,8 @@ class static_reduction_map { * * @return An iterator to one past the last slot */ - __host__ __device__ iterator end() noexcept { return end_slot(); } + __host__ __device__ + constexpr iterator end() noexcept { return end_slot(); } }; public: @@ -624,14 +669,15 @@ class static_reduction_map { * }); * \endcode */ - class device_mutable_view : public device_view_base { + template + class device_mutable_view : public device_view_base { public: - using value_type = typename device_view_base::value_type; - using key_type = typename device_view_base::key_type; - using mapped_type = typename device_view_base::mapped_type; - using iterator = typename device_view_base::iterator; - using const_iterator = typename device_view_base::const_iterator; - using slot_type = typename device_view_base::slot_type; + using value_type = typename device_view_base::value_type; + using key_type = typename device_view_base::key_type; + using mapped_type = typename device_view_base::mapped_type; + using iterator = typename device_view_base::iterator; + using const_iterator = typename device_view_base::const_iterator; + using slot_type = typename device_view_base::slot_type; /** * @brief Construct a mutable view of the first `capacity` slots of the @@ -648,10 +694,12 @@ class static_reduction_map { std::size_t capacity, Key empty_key_sentinel, ReductionOp reduction_op = {}) noexcept - : device_view_base{slots, capacity, empty_key_sentinel, reduction_op} + : device_view_base{slots, capacity, empty_key_sentinel, reduction_op} { } + using device_view_base::device_view_base; + template __device__ static device_mutable_view make_from_uninitialized_slots( CG const& g, @@ -660,9 +708,20 @@ class static_reduction_map { Key empty_key_sentinel, ReductionOp reduction_op = {}) noexcept { - device_view_base::initialize_slots( - g, slots, capacity, empty_key_sentinel, ReductionOp::identity); - return device_mutable_view{slots, capacity, empty_key_sentinel, reduction_op}; + assert(extent == dynamic_extent or capacity == extent); + device_view_base::initialize_slots(g, slots, capacity, empty_key_sentinel, ReductionOp::identity); + return device_mutable_view{slots, capacity, empty_key_sentinel, reduction_op}; + } + + template + __device__ static device_mutable_view make_from_uninitialized_slots( + CG const& g, + slot_type (&slots)[N], + Key empty_key_sentinel, + ReductionOp reduction_op = {}) noexcept + { + device_view_base::initialize_slots(g, slots, N, empty_key_sentinel, ReductionOp::identity); + return device_mutable_view{slots, empty_key_sentinel, reduction_op}; } /** @@ -723,14 +782,15 @@ class static_reduction_map { * value. * */ - class device_view : public device_view_base { + template + class device_view : public device_view_base { public: - using value_type = typename device_view_base::value_type; - using key_type = typename device_view_base::key_type; - using mapped_type = typename device_view_base::mapped_type; - using iterator = typename device_view_base::iterator; - using const_iterator = typename device_view_base::const_iterator; - using slot_type = typename device_view_base::slot_type; + using value_type = typename device_view_base::value_type; + using key_type = typename device_view_base::key_type; + using mapped_type = typename device_view_base::mapped_type; + using iterator = typename device_view_base::iterator; + using const_iterator = typename device_view_base::const_iterator; + using slot_type = typename device_view_base::slot_type; /** * @brief Construct a view of the first `capacity` slots of the @@ -746,7 +806,7 @@ class static_reduction_map { std::size_t capacity, Key empty_key_sentinel, ReductionOp reduction_op = {}) noexcept - : device_view_base{slots, capacity, empty_key_sentinel, reduction_op} + : device_view_base{slots, capacity, empty_key_sentinel, reduction_op} { } @@ -755,8 +815,8 @@ class static_reduction_map { * * @param mutable_map object of type `device_mutable_view` */ - __host__ __device__ explicit device_view(device_mutable_view mutable_map) - : device_view_base{mutable_map.get_slots(), + __host__ __device__ explicit device_view(device_mutable_view mutable_map) + : device_view_base{mutable_map.get_slots(), mutable_map.get_capacity(), mutable_map.get_empty_key_sentinel(), mutable_map.get_op()} @@ -1025,9 +1085,9 @@ class static_reduction_map { * * @return A device_view object based on the members of the `static_reduction_map` object */ - device_view get_device_view() const noexcept + device_view<> get_device_view() const noexcept { - return device_view(slots_, capacity_, empty_key_sentinel_, op_); + return device_view<>(slots_, capacity_, empty_key_sentinel_, op_); } /** @@ -1036,9 +1096,9 @@ class static_reduction_map { * * @return A device_mutable_view object based on the members of the `static_reduction_map` object */ - device_mutable_view get_device_mutable_view() const noexcept + device_mutable_view<> get_device_mutable_view() const noexcept { - return device_mutable_view(slots_, capacity_, empty_key_sentinel_, op_); + return device_mutable_view<>(slots_, capacity_, empty_key_sentinel_, op_); } private: diff --git a/tests/static_reduction_map/static_reduction_map_test.cu b/tests/static_reduction_map/static_reduction_map_test.cu index f1540e041..158093012 100644 --- a/tests/static_reduction_map/static_reduction_map_test.cu +++ b/tests/static_reduction_map/static_reduction_map_test.cu @@ -110,11 +110,11 @@ __global__ void static_reduction_map_shared_memory_kernel(bool* key_found) using Value = typename MapType::mapped_type; namespace cg = cooperative_groups; - using mutable_view_type = typename MapType::device_mutable_view; - using view_type = typename MapType::device_view; + using mutable_view_type = typename MapType::device_mutable_view; + using view_type = typename MapType::device_view; + #pragma diag_suppress static_var_with_dynamic_init __shared__ typename mutable_view_type::slot_type slots[N]; - auto map = - mutable_view_type::make_from_uninitialized_slots(cg::this_thread_block(), &slots[0], N, -1); + auto map = mutable_view_type::make_from_uninitialized_slots(cg::this_thread_block(), slots, -1); auto g = cg::this_thread_block(); std::size_t index = threadIdx.x + blockIdx.x * blockDim.x; From cddd73391204f8137f08037cb633eb0124681c83 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Wed, 13 Oct 2021 16:44:59 -0500 Subject: [PATCH 60/69] Add predicated store to prevent optimization. --- benchmarks/hash_table/static_reduction_map_bench.cu | 3 +++ 1 file changed, 3 insertions(+) diff --git a/benchmarks/hash_table/static_reduction_map_bench.cu b/benchmarks/hash_table/static_reduction_map_bench.cu index 917200290..cf9159fa0 100644 --- a/benchmarks/hash_table/static_reduction_map_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_bench.cu @@ -165,6 +165,9 @@ void static_shmem_insert_kernel(std::size_t num_keys, std::size_t multiplicity, // Placeholder predicated store to inject artificial side-effects and keep compiler from discarding // the code above + if(pred){ + *do_not_use = result; + } } template From c43f7fbfa7c3c9a50b91f3d6c786ff564d57e308 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Wed, 13 Oct 2021 19:24:19 -0500 Subject: [PATCH 61/69] Add multiplicity to benchmark. --- benchmarks/hash_table/static_reduction_map_bench.cu | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/benchmarks/hash_table/static_reduction_map_bench.cu b/benchmarks/hash_table/static_reduction_map_bench.cu index cf9159fa0..3bb9e87aa 100644 --- a/benchmarks/hash_table/static_reduction_map_bench.cu +++ b/benchmarks/hash_table/static_reduction_map_bench.cu @@ -178,7 +178,7 @@ void static_shmem(nvbench::state& state, auto const occupancy = state.get_float64("Occupancy"); auto const num_keys = static_cast(std::floor(Capacity * occupancy)); - auto const multiplicity = 1; + auto const multiplicity = state.get_int64("Multiplicity"); if(num_keys > Capacity){ throw; @@ -208,7 +208,8 @@ NVBENCH_BENCH_TYPES(static_shmem, NVBENCH_TYPE_AXES(key_type_range, value_type_range, op_type_range, capacity_range, extent_options)) .set_name("Insert Static vs Dynamic Extent") .set_type_axes_names({"Key", "Value", "ReductionOp", "Capacity", "Extent"}) - .add_float64_axis("Occupancy", nvbench::range(0.5, 0.9, 0.1)); + .add_int64_axis("Multiplicity", {1}) + .add_float64_axis("Occupancy", nvbench::range(0.1, 0.9, 0.1)); template From bef9aa66b2ef20ed3e07ad7bfe2e3823d772807d Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 14 Oct 2021 07:59:39 -0500 Subject: [PATCH 62/69] Add appropriate host qualifier to device_view_base functions. --- include/cuco/static_reduction_map.cuh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index e12477036..3c816a564 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -534,14 +534,14 @@ class static_reduction_map { * @brief Gets the binary op * */ - __device__ ReductionOp get_op() const noexcept { return op_; } + __host__ __device__ ReductionOp get_op() const noexcept { return op_; } /** * @brief Gets slots array. * * @return Slots array */ - __device__ + __host__ __device__ constexpr slot_type* get_slots() noexcept { return storage_.ptr; } /** @@ -549,7 +549,7 @@ class static_reduction_map { * * @return Slots array */ - __device__ + __host__ __device__ constexpr slot_type const* get_slots() const noexcept { return storage_.ptr; } /** @@ -590,7 +590,7 @@ class static_reduction_map { * * @return Iterator to the first slot */ - __device__ + __host__ __device__ constexpr iterator begin_slot() noexcept { return get_slots(); } /** @@ -606,7 +606,7 @@ class static_reduction_map { * * @return Iterator to the first slot */ - __device__ + __host__ __device__ constexpr const_iterator begin_slot() const noexcept { return get_slots(); } /** From a99b56f758944eb13cbbfe884d6a9e697ff7971c Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 14 Oct 2021 08:00:22 -0500 Subject: [PATCH 63/69] Import extent static member into device_mutable_view. --- include/cuco/static_reduction_map.cuh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 3c816a564..9f6922c7d 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -679,6 +679,8 @@ class static_reduction_map { using const_iterator = typename device_view_base::const_iterator; using slot_type = typename device_view_base::slot_type; + static constexpr std::size_t extent = device_view_base::extent; + /** * @brief Construct a mutable view of the first `capacity` slots of the * slots array pointed to by `slots`. From e87fc9da0bee901c7d27985b6d5cf38113ffa8a6 Mon Sep 17 00:00:00 2001 From: Jake Hemstad Date: Thu, 14 Oct 2021 08:01:03 -0500 Subject: [PATCH 64/69] Add static_assert for factory from static array. Ensure the size of the array is the same as the extent or the extent is dynamic. --- include/cuco/static_reduction_map.cuh | 1 + 1 file changed, 1 insertion(+) diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 9f6922c7d..3dd928878 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -722,6 +722,7 @@ class static_reduction_map { Key empty_key_sentinel, ReductionOp reduction_op = {}) noexcept { + static_assert(extent == dynamic_extent or N == extent); device_view_base::initialize_slots(g, slots, N, empty_key_sentinel, ReductionOp::identity); return device_mutable_view{slots, empty_key_sentinel, reduction_op}; } From 5f244292990dbde9d5311d28ede72e74803250ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20J=C3=BCnger?= Date: Mon, 21 Mar 2022 11:15:30 +0100 Subject: [PATCH 65/69] Minor fixes addressing reviewer comments --- benchmarks/CMakeLists.txt | 2 +- .../static_reduction_map/insert_bench.cu | 1 + .../static_reduction_map/param_sweep.cu | 3 +- .../reduce_by_key/cuco_reduce_by_key_bench.cu | 3 +- benchmarks/utils.hpp | 9 +-- include/cuco/detail/reduction_ops.cuh | 6 +- include/cuco/detail/static_reduction_map.inl | 24 +++---- .../detail/static_reduction_map_kernels.cuh | 62 ++++++++----------- include/cuco/detail/traits.hpp | 54 ---------------- include/cuco/static_reduction_map.cuh | 2 +- tests/CMakeLists.txt | 4 +- 11 files changed, 54 insertions(+), 116 deletions(-) delete mode 100644 include/cuco/detail/traits.hpp diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 927eb790b..c6ab3c868 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -71,7 +71,7 @@ function(ConfigureNVBench BENCH_NAME) endfunction(ConfigureNVBench) ################################################################################################### -### test sources ################################################################################## +### benchmark sources ############################################################################# ################################################################################################### ################################################################################################### diff --git a/benchmarks/hash_table/static_reduction_map/insert_bench.cu b/benchmarks/hash_table/static_reduction_map/insert_bench.cu index 94d3055aa..c71973e59 100644 --- a/benchmarks/hash_table/static_reduction_map/insert_bench.cu +++ b/benchmarks/hash_table/static_reduction_map/insert_bench.cu @@ -15,6 +15,7 @@ */ #include +#include #include #include #include diff --git a/benchmarks/hash_table/static_reduction_map/param_sweep.cu b/benchmarks/hash_table/static_reduction_map/param_sweep.cu index 8f2cd0c22..e3643432a 100644 --- a/benchmarks/hash_table/static_reduction_map/param_sweep.cu +++ b/benchmarks/hash_table/static_reduction_map/param_sweep.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, NVIDIA CORPORATION. + * Copyright (c) 2021-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ * limitations under the License. */ +#include #include #include #include diff --git a/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu index 32dac93b2..59b14dd12 100644 --- a/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu +++ b/benchmarks/reduce_by_key/cuco_reduce_by_key_bench.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, NVIDIA CORPORATION. + * Copyright (c) 2021-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ * limitations under the License. */ +#include #include #include #include diff --git a/benchmarks/utils.hpp b/benchmarks/utils.hpp index a2597e550..a09094378 100644 --- a/benchmarks/utils.hpp +++ b/benchmarks/utils.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, NVIDIA CORPORATION. + * Copyright (c) 2021-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,9 +33,4 @@ std::size_t count_unique(Iter begin, Iter end) std::sort(v.begin(), v.end()); return std::distance(v.begin(), std::unique(v.begin(), v.end())); -} - -// safe division -#ifndef SDIV -#define SDIV(x, y) (((x) + (y)-1) / (y)) -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/include/cuco/detail/reduction_ops.cuh b/include/cuco/detail/reduction_ops.cuh index c8496470e..32ddebdbb 100644 --- a/include/cuco/detail/reduction_ops.cuh +++ b/include/cuco/detail/reduction_ops.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, NVIDIA CORPORATION. + * Copyright (c) 2021-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ #pragma once +#include +#include +#include + namespace cuco { /** diff --git a/include/cuco/detail/static_reduction_map.inl b/include/cuco/detail/static_reduction_map.inl index e40db569d..af6268d3a 100644 --- a/include/cuco/detail/static_reduction_map.inl +++ b/include/cuco/detail/static_reduction_map.inl @@ -69,14 +69,14 @@ template void static_reduction_map::insert( InputIt first, InputIt last, cudaStream_t stream, Hash hash, KeyEqual key_equal) { - auto num_keys = std::distance(first, last); + auto const num_keys = std::distance(first, last); if (num_keys == 0) { return; } - auto const block_size = 128; - auto const stride = 1; - auto const tile_size = 4; - auto const grid_size = (tile_size * num_keys + stride * block_size - 1) / (stride * block_size); - auto view = get_device_mutable_view(); + auto constexpr block_size = 128; + auto constexpr stride = 1; + auto constexpr tile_size = 4; + auto const grid_size = (tile_size * num_keys + stride * block_size - 1) / (stride * block_size); + auto view = get_device_mutable_view(); detail::insert <<>>(first, first + num_keys, view, hash, key_equal); @@ -98,13 +98,13 @@ void static_reduction_map::find(Input auto num_keys = std::distance(first, last); if (num_keys == 0) { return; } - auto const block_size = 128; - auto const stride = 1; - auto const tile_size = 4; - auto const grid_size = (tile_size * num_keys + stride * block_size - 1) / (stride * block_size); - auto view = get_device_view(); + auto constexpr block_size = 128; + auto constexpr stride = 1; + auto constexpr tile_size = 4; + auto const grid_size = (tile_size * num_keys + stride * block_size - 1) / (stride * block_size); + auto const view = get_device_view(); - detail::find + detail::find <<>>(first, last, output_begin, view, hash, key_equal); } diff --git a/include/cuco/detail/static_reduction_map_kernels.cuh b/include/cuco/detail/static_reduction_map_kernels.cuh index 7dbad0c4a..8dce498e6 100644 --- a/include/cuco/detail/static_reduction_map_kernels.cuh +++ b/include/cuco/detail/static_reduction_map_kernels.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,10 @@ * limitations under the License. */ +#include +#include +#include + namespace cuco { namespace detail { namespace cg = cooperative_groups; @@ -65,7 +69,6 @@ __global__ void initialize(pair_atomic_type* const slots, Key k, Value v, std::s * @tparam KeyEqual Binary callable type * @param first Beginning of the sequence of key/value pairs * @param last End of the sequence of key/value pairs - * @param num_successes The number of successfully inserted key/value pairs * @param view Mutable device view used to access the hash map's slot storage * @param hash The unary function to apply to hash each key * @param key_equal The binary function used to compare two keys for equality @@ -77,8 +80,8 @@ template __global__ void insert(InputIt first, InputIt last, viewT view, Hash hash, KeyEqual key_equal) { - auto tid = block_size * blockIdx.x + threadIdx.x; - auto it = first + tid; + auto const tid = block_size * blockIdx.x + threadIdx.x; + auto it = first + tid; while (it < last) { typename viewT::value_type const insert_pair{*it}; @@ -101,36 +104,29 @@ __global__ void insert(InputIt first, InputIt last, viewT view, Hash hash, KeyEq * inserts * @tparam InputIt Device accessible input iterator whose `value_type` is * convertible to the map's `value_type` - * @tparam atomicT Type of atomic storage * @tparam viewT Type of device view allowing access of hash map storage * @tparam Hash Unary callable type * @tparam KeyEqual Binary callable type * @param first Beginning of the sequence of key/value pairs * @param last End of the sequence of key/value pairs - * @param num_successes The number of successfully inserted key/value pairs * @param view Mutable device view used to access the hash map's slot storage * @param hash The unary function to apply to hash each key * @param key_equal The binary function used to compare two keys for equality */ template __global__ void insert(InputIt first, InputIt last, viewT view, Hash hash, KeyEqual key_equal) { - auto tile = cg::tiled_partition(cg::this_thread_block()); - auto tid = block_size * blockIdx.x + threadIdx.x; - auto it = first + tid / tile_size; + auto const tile = cg::tiled_partition(cg::this_thread_block()); + auto const tid = block_size * blockIdx.x + threadIdx.x; + auto it = first + tid / tile_size; while (it < last) { - // force conversion to value_type - typename viewT::value_type const insert_pair{ - static_cast(thrust::get<0>(*it)), - static_cast(thrust::get<1>(*it))}; - - view.insert(tile, insert_pair, hash, key_equal); + view.insert(tile, *it, hash, key_equal); it += (gridDim.x * block_size) / tile_size; } } @@ -141,7 +137,6 @@ __global__ void insert(InputIt first, InputIt last, viewT view, Hash hash, KeyEq * If the key `*(first + i)` exists in the map, copies its associated value to `(output_begin + i)`. * Else, copies the empty value sentinel. * @tparam block_size The size of the thread block - * @tparam Value The type of the mapped value for the map * @tparam InputIt Device accessible input iterator whose `value_type` is * convertible to the map's `key_type` * @tparam OutputIt Device accessible output iterator whose `value_type` is @@ -157,7 +152,6 @@ __global__ void insert(InputIt first, InputIt last, viewT view, Hash hash, KeyEq * @param key_equal The binary function to compare two keys for equality */ template (cg::this_thread_block()); - auto tid = block_size * blockIdx.x + threadIdx.x; - auto key_idx = tid / tile_size; - __shared__ Value writeBuffer[block_size]; + auto const tile = cg::tiled_partition(cg::this_thread_block()); + auto const tid = block_size * blockIdx.x + threadIdx.x; + auto key_idx = tid / tile_size; + __shared__ typename viewT::mapped_type writeBuffer[block_size]; while (first + key_idx < last) { auto key = *(first + key_idx); @@ -261,7 +253,7 @@ __global__ void find( * @tparam InputIt Device accessible input iterator whose `value_type` is * convertible to the map's `key_type` * @tparam OutputIt Device accessible output iterator whose `value_type` is - * convertible to the map's `mapped_type` + * convertible to `bool` * @tparam viewT Type of device view allowing access of hash map storage * @tparam Hash Unary callable type * @tparam KeyEqual Binary callable type @@ -281,8 +273,8 @@ template (cg::this_thread_block()); - auto tid = block_size * blockIdx.x + threadIdx.x; - auto key_idx = tid / tile_size; + auto const tile = cg::tiled_partition(cg::this_thread_block()); + auto const tid = block_size * blockIdx.x + threadIdx.x; + auto key_idx = tid / tile_size; __shared__ bool writeBuffer[block_size]; while (first + key_idx < last) { diff --git a/include/cuco/detail/traits.hpp b/include/cuco/detail/traits.hpp deleted file mode 100644 index 53ef38433..000000000 --- a/include/cuco/detail/traits.hpp +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2021, 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 - */ - -#pragma once - -namespace cuco { -/** - * @brief Customization point that can be specialized to indicate that it is safe to perform bitwise - * equality comparisons on objects of type `T`. - * - * By default, only types where `std::has_unique_object_representations_v` is true are safe for - * bitwise equality. However, this can be too restrictive for some types, e.g., floating point - * types. - * - * User-defined specializations of `is_bitwise_comparable` are allowed, but it is the users - * responsibility to ensure values do not occur that would lead to unexpected behavior. For example, - * if a `NaN` bit pattern were used as the empty sentinel value, it may not compare bitwise equal to - * other `NaN` bit patterns. - * - */ -template -struct is_bitwise_comparable : std::false_type { -}; - -/// By default, only types with unique object representations are allowed -template -struct is_bitwise_comparable>> - : std::true_type { -}; - -/** - * @brief Declares that a type `Type` is bitwise comparable. - * - */ -#define CUCO_DECLARE_BITWISE_COMPARABLE(Type) \ - namespace cuco { \ - template <> \ - struct is_bitwise_comparable : std::true_type { \ - }; \ - } - -} // namespace cuco \ No newline at end of file diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 52c373e61..27bda61e9 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -42,7 +42,7 @@ #include #include #include -#include +#include #include namespace cuco { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e22b2f49e..fba49c0b1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,5 @@ #============================================================================= -# Copyright (c) 2018-2021, NVIDIA CORPORATION. +# Copyright (c) 2018-2022, NVIDIA CORPORATION. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -43,8 +43,6 @@ function(ConfigureTest TEST_NAME) target_include_directories(${TEST_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) set_target_properties(${TEST_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") - target_include_directories(${TEST_NAME} PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}") target_compile_options(${TEST_NAME} PRIVATE --expt-extended-lambda --expt-relaxed-constexpr -Xcompiler -Wno-subobject-linkage) catch_discover_tests(${TEST_NAME}) endfunction(ConfigureTest) From c2e4e6212ee612a05ef873b9d2b988efa5d4e560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20J=C3=BCnger?= Date: Wed, 23 Mar 2022 15:38:25 +0100 Subject: [PATCH 66/69] Move reduction operators to include/cuco/ --- include/cuco/{detail => }/reduction_ops.cuh | 0 include/cuco/static_reduction_map.cuh | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename include/cuco/{detail => }/reduction_ops.cuh (100%) diff --git a/include/cuco/detail/reduction_ops.cuh b/include/cuco/reduction_ops.cuh similarity index 100% rename from include/cuco/detail/reduction_ops.cuh rename to include/cuco/reduction_ops.cuh diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 27bda61e9..1fdba3cb1 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -40,8 +40,8 @@ #include #include #include -#include #include +#include #include #include From e1361a3cad8105402315b47c6441237a192c0de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20J=C3=BCnger?= Date: Wed, 23 Mar 2022 16:15:52 +0100 Subject: [PATCH 67/69] Added a tag to ensure that only valid reduction functors can be used --- include/cuco/detail/tags.hpp | 30 +++++++++++++++ include/cuco/reduction_ops.cuh | 53 ++++++++++++++++++++------- include/cuco/static_reduction_map.cuh | 4 +- 3 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 include/cuco/detail/tags.hpp diff --git a/include/cuco/detail/tags.hpp b/include/cuco/detail/tags.hpp new file mode 100644 index 000000000..7520e6e4d --- /dev/null +++ b/include/cuco/detail/tags.hpp @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022, 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 + +namespace cuco { + +namespace detail { + +namespace tags { + +struct reduction_op { +}; + +} // namespace tags +} // namespace detail +} // namespace cuco \ No newline at end of file diff --git a/include/cuco/reduction_ops.cuh b/include/cuco/reduction_ops.cuh index 32ddebdbb..ec3d5a936 100644 --- a/include/cuco/reduction_ops.cuh +++ b/include/cuco/reduction_ops.cuh @@ -17,6 +17,7 @@ #pragma once #include +#include #include #include @@ -30,7 +31,9 @@ namespace cuco { */ template struct reduce_add { - using value_type = T; + using value_type = T; + using tag = detail::tags::reduction_op; + static constexpr T identity = 0; template @@ -44,7 +47,9 @@ struct reduce_add { // https://github.com/NVIDIA/libcudacxx/issues/104 template <> struct reduce_add { - using value_type = float; + using value_type = float; + using tag = detail::tags::reduction_op; + static constexpr float identity = 0; template @@ -56,7 +61,9 @@ struct reduce_add { template <> struct reduce_add { - using value_type = double; + using value_type = double; + using tag = detail::tags::reduction_op; + static constexpr double identity = 0; template @@ -74,7 +81,9 @@ struct reduce_add { */ template struct reduce_sub { - using value_type = T; + using value_type = T; + using tag = detail::tags::reduction_op; + static constexpr T identity = 0; template @@ -86,7 +95,9 @@ struct reduce_sub { template <> struct reduce_sub { - using value_type = float; + using value_type = float; + using tag = detail::tags::reduction_op; + static constexpr float identity = 0; template @@ -98,7 +109,9 @@ struct reduce_sub { template <> struct reduce_sub { - using value_type = double; + using value_type = double; + using tag = detail::tags::reduction_op; + static constexpr double identity = 0; template @@ -116,7 +129,9 @@ struct reduce_sub { */ template struct reduce_min { - using value_type = T; + using value_type = T; + using tag = detail::tags::reduction_op; + static constexpr T identity = std::numeric_limits::max(); template @@ -128,7 +143,9 @@ struct reduce_min { template <> struct reduce_min { - using value_type = float; + using value_type = float; + using tag = detail::tags::reduction_op; + static constexpr float identity = std::numeric_limits::max(); template @@ -140,7 +157,9 @@ struct reduce_min { template <> struct reduce_min { - using value_type = double; + using value_type = double; + using tag = detail::tags::reduction_op; + static constexpr double identity = std::numeric_limits::max(); template @@ -158,7 +177,9 @@ struct reduce_min { */ template struct reduce_max { - using value_type = T; + using value_type = T; + using tag = detail::tags::reduction_op; + static constexpr T identity = std::numeric_limits::lowest(); template @@ -170,7 +191,9 @@ struct reduce_max { template <> struct reduce_max { - using value_type = float; + using value_type = float; + using tag = detail::tags::reduction_op; + static constexpr float identity = std::numeric_limits::lowest(); template @@ -182,7 +205,9 @@ struct reduce_max { template <> struct reduce_max { - using value_type = double; + using value_type = double; + using tag = detail::tags::reduction_op; + static constexpr double identity = std::numeric_limits::lowest(); template @@ -206,7 +231,9 @@ template struct custom_op { - using value_type = T; + using value_type = T; + using tag = detail::tags::reduction_op; + static constexpr T identity = Identity; Op op; diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 1fdba3cb1..62d6be8e2 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -151,7 +152,8 @@ class static_reduction_map { is_bitwise_comparable::value, "Key type must have unique object representations or have been explicitly declared as safe for " "bitwise comparison via specialization of cuco::is_bitwise_comparable."); - + static_assert(std::is_same::value, + "Invalid reduction functor"); static_assert(std::is_same::value, "Type mismatch between ReductionOp::value_type and Value"); From d196de5a71373c8df317b1e96a5cf10d61a0515a Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Thu, 26 May 2022 14:53:12 -0400 Subject: [PATCH 68/69] Move common kernels to a new file --- include/cuco/detail/common_kernels.cuh | 172 +++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 include/cuco/detail/common_kernels.cuh diff --git a/include/cuco/detail/common_kernels.cuh b/include/cuco/detail/common_kernels.cuh new file mode 100644 index 000000000..e97caa564 --- /dev/null +++ b/include/cuco/detail/common_kernels.cuh @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2020-2022, 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 cuco { +namespace detail { +namespace cg = cooperative_groups; + +/** + * @brief Initializes each slot in the flat `slots` storage to contain `k` and `v`. + * + * Each space in `slots` that can hold a key value pair is initialized to a + * `pair_atomic_type` containing the key `k` and the value `v`. + * + * @tparam atomic_key_type Type of the `Key` atomic container + * @tparam atomic_mapped_type Type of the `Value` atomic container + * @tparam Key key type + * @tparam Value value type + * @tparam pair_atomic_type key/value pair type + * @param slots Pointer to flat storage for the map's key/value pairs + * @param k Key to which all keys in `slots` are initialized + * @param v Value to which all values in `slots` are initialized + * @param size Size of the storage pointed to by `slots` + */ +template +__global__ void initialize(pair_atomic_type* const slots, Key k, Value v, std::size_t size) +{ + auto tid = block_size * blockIdx.x + threadIdx.x; + while (tid < size) { + new (&slots[tid].first) atomic_key_type{k}; + new (&slots[tid].second) atomic_mapped_type{v}; + tid += gridDim.x * block_size; + } +} + +/** + * @brief Indicates whether the keys in the range `[first, last)` are contained in the map. + * + * Writes a `bool` to `(output + i)` indicating if the key `*(first + i)` exists in the map. + * + * @tparam block_size The size of the thread block + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the map's `key_type` + * @tparam OutputIt Device accessible output iterator whose `value_type` is + * convertible to `bool` + * @tparam viewT Type of device view allowing access of hash map storage + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param output_begin Beginning of the sequence of booleans for the presence of each key + * @param view Device view used to access the hash map's slot storage + * @param hash The unary function to apply to hash each key + * @param key_equal The binary function to compare two keys for equality + */ +template +__global__ void contains( + InputIt first, InputIt last, OutputIt output_begin, viewT view, Hash hash, KeyEqual key_equal) +{ + auto const tid = block_size * blockIdx.x + threadIdx.x; + auto key_idx = tid; + __shared__ bool writeBuffer[block_size]; + + while (first + key_idx < last) { + auto key = *(first + key_idx); + + /* + * The ld.relaxed.gpu instruction used in view.find causes L1 to + * flush more frequently, causing increased sector stores from L2 to global memory. + * By writing results to shared memory and then synchronizing before writing back + * to global, we no longer rely on L1, preventing the increase in sector stores from + * L2 to global and improving performance. + */ + writeBuffer[threadIdx.x] = view.contains(key, hash, key_equal); + __syncthreads(); + *(output_begin + key_idx) = writeBuffer[threadIdx.x]; + key_idx += gridDim.x * block_size; + } +} + +/** + * @brief Indicates whether the keys in the range `[first, last)` are contained in the map. + * + * Writes a `bool` to `(output + i)` indicating if the key `*(first + i)` exists in the map. + * Uses the CUDA Cooperative Groups API to leverage groups of multiple threads to perform the + * contains operation for each key. This provides a significant boost in throughput compared + * to the non Cooperative Group `contains` at moderate to high load factors. + * + * @tparam block_size The size of the thread block + * @tparam tile_size The number of threads in the Cooperative Groups used to perform + * inserts + * @tparam InputIt Device accessible input iterator whose `value_type` is + * convertible to the map's `key_type` + * @tparam OutputIt Device accessible output iterator whose `value_type` is + * convertible to `bool` + * @tparam viewT Type of device view allowing access of hash map storage + * @tparam Hash Unary callable type + * @tparam KeyEqual Binary callable type + * @param first Beginning of the sequence of keys + * @param last End of the sequence of keys + * @param output_begin Beginning of the sequence of booleans for the presence of each key + * @param view Device view used to access the hash map's slot storage + * @param hash The unary function to apply to hash each key + * @param key_equal The binary function to compare two keys for equality + */ +template +__global__ void contains( + InputIt first, InputIt last, OutputIt output_begin, viewT view, Hash hash, KeyEqual key_equal) +{ + auto const tile = cg::tiled_partition(cg::this_thread_block()); + auto const tid = block_size * blockIdx.x + threadIdx.x; + auto key_idx = tid / tile_size; + __shared__ bool writeBuffer[block_size]; + + while (first + key_idx < last) { + auto key = *(first + key_idx); + auto found = view.contains(tile, key, hash, key_equal); + + /* + * The ld.relaxed.gpu instruction used in view.find causes L1 to + * flush more frequently, causing increased sector stores from L2 to global memory. + * By writing results to shared memory and then synchronizing before writing back + * to global, we no longer rely on L1, preventing the increase in sector stores from + * L2 to global and improving performance. + */ + if (tile.thread_rank() == 0) { writeBuffer[threadIdx.x / tile_size] = found; } + __syncthreads(); + if (tile.thread_rank() == 0) { + *(output_begin + key_idx) = writeBuffer[threadIdx.x / tile_size]; + } + key_idx += (gridDim.x * block_size) / tile_size; + } +} + +} // namespace detail +} // namespace cuco From e904dca1dc349c7f83b6bf07dfd03048381be869 Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Thu, 26 May 2022 14:55:03 -0400 Subject: [PATCH 69/69] Updates: incorporate new kernel header --- .../static_reduction_map_example.cu | 2 +- include/cuco/detail/static_map_kernels.cuh | 147 +----------------- .../detail/static_reduction_map_kernels.cuh | 145 +---------------- include/cuco/static_map.cuh | 24 +-- include/cuco/static_reduction_map.cuh | 26 ++-- 5 files changed, 31 insertions(+), 313 deletions(-) diff --git a/examples/static_reduction_map/static_reduction_map_example.cu b/examples/static_reduction_map/static_reduction_map_example.cu index 8d3839658..3173b47ce 100644 --- a/examples/static_reduction_map/static_reduction_map_example.cu +++ b/examples/static_reduction_map/static_reduction_map_example.cu @@ -90,4 +90,4 @@ int main(void) for (int i = 0; i < unique_keys.size(); ++i) { std::cout << "Key: " << unique_keys[i] << " Count: " << count_per_key[i] << std::endl; } -} \ No newline at end of file +} diff --git a/include/cuco/detail/static_map_kernels.cuh b/include/cuco/detail/static_map_kernels.cuh index 642373135..ce886ec8a 100644 --- a/include/cuco/detail/static_map_kernels.cuh +++ b/include/cuco/detail/static_map_kernels.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,43 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#pragma once + +#include namespace cuco { namespace detail { namespace cg = cooperative_groups; -/** - * @brief Initializes each slot in the flat `slots` storage to contain `k` and `v`. - * - * Each space in `slots` that can hold a key value pair is initialized to a - * `pair_atomic_type` containing the key `k` and the value `v`. - * - * @tparam atomic_key_type Type of the `Key` atomic container - * @tparam atomic_mapped_type Type of the `Value` atomic container - * @tparam Key key type - * @tparam Value value type - * @tparam pair_atomic_type key/value pair type - * @param slots Pointer to flat storage for the map's key/value pairs - * @param k Key to which all keys in `slots` are initialized - * @param v Value to which all values in `slots` are initialized - * @param size Size of the storage pointed to by `slots` - */ -template -__global__ void initialize(pair_atomic_type* const slots, Key k, Value v, std::size_t size) -{ - auto tid = block_size * blockIdx.x + threadIdx.x; - while (tid < size) { - new (&slots[tid].first) atomic_key_type{k}; - new (&slots[tid].second) atomic_mapped_type{v}; - tid += gridDim.x * block_size; - } -} - /** * @brief Inserts all key/value pairs in the range `[first, last)`. * @@ -349,115 +320,5 @@ __global__ void find( } } -/** - * @brief Indicates whether the keys in the range `[first, last)` are contained in the map. - * - * Writes a `bool` to `(output + i)` indicating if the key `*(first + i)` exists in the map. - * - * @tparam block_size The size of the thread block - * @tparam InputIt Device accessible input iterator whose `value_type` is - * convertible to the map's `key_type` - * @tparam OutputIt Device accessible output iterator whose `value_type` is - * convertible to the map's `mapped_type` - * @tparam viewT Type of device view allowing access of hash map storage - * @tparam Hash Unary callable type - * @tparam KeyEqual Binary callable type - * @param first Beginning of the sequence of keys - * @param last End of the sequence of keys - * @param output_begin Beginning of the sequence of booleans for the presence of each key - * @param view Device view used to access the hash map's slot storage - * @param hash The unary function to apply to hash each key - * @param key_equal The binary function to compare two keys for equality - */ -template -__global__ void contains( - InputIt first, InputIt last, OutputIt output_begin, viewT view, Hash hash, KeyEqual key_equal) -{ - auto tid = block_size * blockIdx.x + threadIdx.x; - auto key_idx = tid; - __shared__ bool writeBuffer[block_size]; - - while (first + key_idx < last) { - auto key = *(first + key_idx); - - /* - * The ld.relaxed.gpu instruction used in view.find causes L1 to - * flush more frequently, causing increased sector stores from L2 to global memory. - * By writing results to shared memory and then synchronizing before writing back - * to global, we no longer rely on L1, preventing the increase in sector stores from - * L2 to global and improving performance. - */ - writeBuffer[threadIdx.x] = view.contains(key, hash, key_equal); - __syncthreads(); - *(output_begin + key_idx) = writeBuffer[threadIdx.x]; - key_idx += gridDim.x * block_size; - } -} - -/** - * @brief Indicates whether the keys in the range `[first, last)` are contained in the map. - * - * Writes a `bool` to `(output + i)` indicating if the key `*(first + i)` exists in the map. - * Uses the CUDA Cooperative Groups API to leverage groups of multiple threads to perform the - * contains operation for each key. This provides a significant boost in throughput compared - * to the non Cooperative Group `contains` at moderate to high load factors. - * - * @tparam block_size The size of the thread block - * @tparam tile_size The number of threads in the Cooperative Groups used to perform - * inserts - * @tparam InputIt Device accessible input iterator whose `value_type` is - * convertible to the map's `key_type` - * @tparam OutputIt Device accessible output iterator whose `value_type` is - * convertible to the map's `mapped_type` - * @tparam viewT Type of device view allowing access of hash map storage - * @tparam Hash Unary callable type - * @tparam KeyEqual Binary callable type - * @param first Beginning of the sequence of keys - * @param last End of the sequence of keys - * @param output_begin Beginning of the sequence of booleans for the presence of each key - * @param view Device view used to access the hash map's slot storage - * @param hash The unary function to apply to hash each key - * @param key_equal The binary function to compare two keys for equality - */ -template -__global__ void contains( - InputIt first, InputIt last, OutputIt output_begin, viewT view, Hash hash, KeyEqual key_equal) -{ - auto tile = cg::tiled_partition(cg::this_thread_block()); - auto tid = block_size * blockIdx.x + threadIdx.x; - auto key_idx = tid / tile_size; - __shared__ bool writeBuffer[block_size]; - - while (first + key_idx < last) { - auto key = *(first + key_idx); - auto found = view.contains(tile, key, hash, key_equal); - - /* - * The ld.relaxed.gpu instruction used in view.find causes L1 to - * flush more frequently, causing increased sector stores from L2 to global memory. - * By writing results to shared memory and then synchronizing before writing back - * to global, we no longer rely on L1, preventing the increase in sector stores from - * L2 to global and improving performance. - */ - if (tile.thread_rank() == 0) { writeBuffer[threadIdx.x / tile_size] = found; } - __syncthreads(); - if (tile.thread_rank() == 0) { - *(output_begin + key_idx) = writeBuffer[threadIdx.x / tile_size]; - } - key_idx += (gridDim.x * block_size) / tile_size; - } -} - } // namespace detail } // namespace cuco diff --git a/include/cuco/detail/static_reduction_map_kernels.cuh b/include/cuco/detail/static_reduction_map_kernels.cuh index 8dce498e6..c8588fdec 100644 --- a/include/cuco/detail/static_reduction_map_kernels.cuh +++ b/include/cuco/detail/static_reduction_map_kernels.cuh @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#pragma once #include #include @@ -22,38 +23,6 @@ namespace cuco { namespace detail { namespace cg = cooperative_groups; -/** - * @brief Initializes each slot in the flat `slots` storage to contain `k` and `v`. - * - * Each space in `slots` that can hold a key value pair is initialized to a - * `pair_atomic_type` containing the key `k` and the value `v`. - * - * @tparam atomic_key_type Type of the `Key` atomic container - * @tparam atomic_mapped_type Type of the `Value` atomic container - * @tparam Key key type - * @tparam Value value type - * @tparam pair_atomic_type key/value pair type - * @param slots Pointer to flat storage for the map's key/value pairs - * @param k Key to which all keys in `slots` are initialized - * @param v Value to which all values in `slots` are initialized - * @param size Size of the storage pointed to by `slots` - */ -template -__global__ void initialize(pair_atomic_type* const slots, Key k, Value v, std::size_t size) -{ - auto tid = block_size * blockIdx.x + threadIdx.x; - while (tid < size) { - new (&slots[tid].first) atomic_key_type{k}; - new (&slots[tid].second) atomic_mapped_type{v}; - tid += gridDim.x * block_size; - } -} - /** * @brief Inserts all key/value pairs in the range `[first, last)`. * @@ -244,115 +213,5 @@ __global__ void find( } } -/** - * @brief Indicates whether the keys in the range `[first, last)` are contained in the map. - * - * Writes a `bool` to `(output + i)` indicating if the key `*(first + i)` exists in the map. - * - * @tparam block_size The size of the thread block - * @tparam InputIt Device accessible input iterator whose `value_type` is - * convertible to the map's `key_type` - * @tparam OutputIt Device accessible output iterator whose `value_type` is - * convertible to `bool` - * @tparam viewT Type of device view allowing access of hash map storage - * @tparam Hash Unary callable type - * @tparam KeyEqual Binary callable type - * @param first Beginning of the sequence of keys - * @param last End of the sequence of keys - * @param output_begin Beginning of the sequence of booleans for the presence of each key - * @param view Device view used to access the hash map's slot storage - * @param hash The unary function to apply to hash each key - * @param key_equal The binary function to compare two keys for equality - */ -template -__global__ void contains( - InputIt first, InputIt last, OutputIt output_begin, viewT view, Hash hash, KeyEqual key_equal) -{ - auto const tid = block_size * blockIdx.x + threadIdx.x; - auto key_idx = tid; - __shared__ bool writeBuffer[block_size]; - - while (first + key_idx < last) { - auto key = *(first + key_idx); - - /* - * The ld.relaxed.gpu instruction used in view.find causes L1 to - * flush more frequently, causing increased sector stores from L2 to global memory. - * By writing results to shared memory and then synchronizing before writing back - * to global, we no longer rely on L1, preventing the increase in sector stores from - * L2 to global and improving performance. - */ - writeBuffer[threadIdx.x] = view.contains(key, hash, key_equal); - __syncthreads(); - *(output_begin + key_idx) = writeBuffer[threadIdx.x]; - key_idx += gridDim.x * block_size; - } -} - -/** - * @brief Indicates whether the keys in the range `[first, last)` are contained in the map. - * - * Writes a `bool` to `(output + i)` indicating if the key `*(first + i)` exists in the map. - * Uses the CUDA Cooperative Groups API to leverage groups of multiple threads to perform the - * contains operation for each key. This provides a significant boost in throughput compared - * to the non Cooperative Group `contains` at moderate to high load factors. - * - * @tparam block_size The size of the thread block - * @tparam tile_size The number of threads in the Cooperative Groups used to perform - * inserts - * @tparam InputIt Device accessible input iterator whose `value_type` is - * convertible to the map's `key_type` - * @tparam OutputIt Device accessible output iterator whose `value_type` is - * convertible to `bool` - * @tparam viewT Type of device view allowing access of hash map storage - * @tparam Hash Unary callable type - * @tparam KeyEqual Binary callable type - * @param first Beginning of the sequence of keys - * @param last End of the sequence of keys - * @param output_begin Beginning of the sequence of booleans for the presence of each key - * @param view Device view used to access the hash map's slot storage - * @param hash The unary function to apply to hash each key - * @param key_equal The binary function to compare two keys for equality - */ -template -__global__ void contains( - InputIt first, InputIt last, OutputIt output_begin, viewT view, Hash hash, KeyEqual key_equal) -{ - auto const tile = cg::tiled_partition(cg::this_thread_block()); - auto const tid = block_size * blockIdx.x + threadIdx.x; - auto key_idx = tid / tile_size; - __shared__ bool writeBuffer[block_size]; - - while (first + key_idx < last) { - auto key = *(first + key_idx); - auto found = view.contains(tile, key, hash, key_equal); - - /* - * The ld.relaxed.gpu instruction used in view.find causes L1 to - * flush more frequently, causing increased sector stores from L2 to global memory. - * By writing results to shared memory and then synchronizing before writing back - * to global, we no longer rely on L1, preventing the increase in sector stores from - * L2 to global and improving performance. - */ - if (tile.thread_rank() == 0) { writeBuffer[threadIdx.x / tile_size] = found; } - __syncthreads(); - if (tile.thread_rank() == 0) { - *(output_begin + key_idx) = writeBuffer[threadIdx.x / tile_size]; - } - key_idx += (gridDim.x * block_size) / tile_size; - } -} - } // namespace detail -} // namespace cuco \ No newline at end of file +} // namespace cuco diff --git a/include/cuco/static_map.cuh b/include/cuco/static_map.cuh index 199dcd838..9aedb3e5a 100644 --- a/include/cuco/static_map.cuh +++ b/include/cuco/static_map.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021, NVIDIA CORPORATION. + * Copyright (c) 2020-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,17 @@ #pragma once -#include -#include -#include -#include -#include -#include - #include +#include +#include +#include +#include +#include #include +#include + +#include #if defined(CUDART_VERSION) && (CUDART_VERSION >= 11000) && defined(__CUDA_ARCH__) && \ (__CUDA_ARCH__ >= 700) #define CUCO_HAS_CUDA_BARRIER @@ -35,10 +36,9 @@ #include #endif -#include -#include -#include -#include +#include +#include +#include namespace cuco { diff --git a/include/cuco/static_reduction_map.cuh b/include/cuco/static_reduction_map.cuh index 62d6be8e2..be87f8b47 100644 --- a/include/cuco/static_reduction_map.cuh +++ b/include/cuco/static_reduction_map.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, NVIDIA CORPORATION. + * Copyright (c) 2021-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,17 @@ */ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include -#include #include #include #include @@ -26,8 +35,6 @@ #include #include -#include - #if defined(CUDART_VERSION) && (CUDART_VERSION >= 11000) && defined(__CUDA_ARCH__) && \ (__CUDA_ARCH__ >= 700) #define CUCO_HAS_CUDA_BARRIER @@ -37,15 +44,6 @@ #include #endif -#include -#include -#include -#include -#include -#include -#include -#include - namespace cuco { static constexpr std::size_t dynamic_extent = std::numeric_limits::max(); @@ -1130,4 +1128,4 @@ class static_reduction_map { }; } // namespace cuco -#include \ No newline at end of file +#include