#pragma once #include #include #include #include #include #include #include #include #include #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA # include # include # include #endif // THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA #include #include #include #if _CCCL_HAS_INT128() using int128_t = __int128_t; using uint128_t = __uint128_t; NVBENCH_DECLARE_TYPE_STRINGS(int128_t, "I128", "int128_t"); NVBENCH_DECLARE_TYPE_STRINGS(uint128_t, "U128", "uint128_t"); #endif using complex32 = cuda::std::complex; using complex64 = cuda::std::complex; #if _CCCL_HAS_NVFP16() NVBENCH_DECLARE_TYPE_STRINGS(__half, "F16", "half"); NVBENCH_DECLARE_TYPE_STRINGS(cuda::std::complex<__half>, "C16", "complex_half"); #endif #if _CCCL_HAS_NVBF16() NVBENCH_DECLARE_TYPE_STRINGS(__nv_bfloat16, "BF16", "bfloat16"); NVBENCH_DECLARE_TYPE_STRINGS(cuda::std::complex<__nv_bfloat16>, "CB16", "complex_bfloat16"); #endif NVBENCH_DECLARE_TYPE_STRINGS(complex32, "C32", "complex32"); NVBENCH_DECLARE_TYPE_STRINGS(complex64, "C64", "complex64"); NVBENCH_DECLARE_TYPE_STRINGS(::cuda::std::false_type, "false", "false_type"); NVBENCH_DECLARE_TYPE_STRINGS(::cuda::std::true_type, "true", "true_type"); NVBENCH_DECLARE_TYPE_STRINGS(cub::detail::arg_min, "arg_min", "cub::detail::arg_min"); NVBENCH_DECLARE_TYPE_STRINGS(cub::detail::arg_max, "arg_max", "cub::detail::arg_max"); template struct nvbench::type_strings<::cuda::std::integral_constant> { static std::string input_string() { return std::to_string(I); } static std::string description() { return "integral_constant<" + type_strings::description() + ", " + std::to_string(I) + ">"; } }; namespace detail { template struct push_back {}; template struct push_back, Ts...> { using type = nvbench::type_list; }; } // namespace detail template using push_back_t = typename detail::push_back::type; #ifdef TUNE_OffsetT using offset_types = nvbench::type_list; #else using offset_types = nvbench::type_list; #endif #ifdef TUNE_T using integral_types = nvbench::type_list; using fundamental_types = nvbench::type_list; using all_types = nvbench::type_list; #else // keep those lists in sync with the documentation in tuning_infra.rst using integral_types = nvbench::type_list; using fundamental_types = nvbench::type_list; using all_types = nvbench::type_list; #endif template class value_wrapper_t { T m_val{}; public: explicit value_wrapper_t(T val) : m_val(val) {} T get() const { return m_val; } value_wrapper_t& operator++() { m_val++; return *this; } }; class seed_t : public value_wrapper_t { public: using value_wrapper_t::value_wrapper_t; using value_wrapper_t::operator++; seed_t() : value_wrapper_t(42) {} }; enum class bit_entropy { _1_000 = 0, _0_811 = 1, _0_544 = 2, _0_337 = 3, _0_201 = 4, _0_000 = 4200 }; NVBENCH_DECLARE_TYPE_STRINGS(bit_entropy, "BE", "bit entropy"); [[nodiscard]] inline double entropy_to_probability(bit_entropy entropy) { switch (entropy) { case bit_entropy::_1_000: return 1.0; case bit_entropy::_0_811: return 0.811; case bit_entropy::_0_544: return 0.544; case bit_entropy::_0_337: return 0.337; case bit_entropy::_0_201: return 0.201; case bit_entropy::_0_000: [[fallthrough]]; default: return 0.0; } } [[nodiscard]] inline bit_entropy str_to_entropy(std::string str) { if (str == "1.000") { return bit_entropy::_1_000; } else if (str == "0.811") { return bit_entropy::_0_811; } else if (str == "0.544") { return bit_entropy::_0_544; } else if (str == "0.337") { return bit_entropy::_0_337; } else if (str == "0.201") { return bit_entropy::_0_201; } else if (str == "0.000") { return bit_entropy::_0_000; } throw std::runtime_error("Can't convert string to bit entropy"); } // Creates an interpolated value of type T between min (at = 0.0) and max (at = 1.0). template [[nodiscard]] T lerp_min_max(double at) noexcept { if (at == 1.0) { return ::cuda::std::numeric_limits::max(); } const auto min_val = static_cast(::cuda::std::numeric_limits::lowest()); const auto max_val = static_cast(::cuda::std::numeric_limits::max()); return static_cast(::cuda::std::lerp(min_val, max_val, at)); } namespace detail { void do_not_optimize(const void* ptr); template void gen_host(seed_t seed, cuda::std::span data, bit_entropy entropy, T min, T max); template void gen_device(seed_t seed, cuda::std::span data, bit_entropy entropy, T min, T max); template void gen_uniform_key_segments_host( seed_t seed, cuda::std::span data, std::size_t min_segment_size, std::size_t max_segment_size); template void gen_uniform_key_segments_device( seed_t seed, cuda::std::span data, std::size_t min_segment_size, std::size_t max_segment_size); template std::size_t gen_uniform_segment_offsets_host( seed_t seed, cuda::std::span segment_offsets, std::size_t min_segment_size, std::size_t max_segment_size); template std::size_t gen_uniform_segment_offsets_device( seed_t seed, cuda::std::span segment_offsets, std::size_t min_segment_size, std::size_t max_segment_size); template void gen_power_law_segment_offsets_host(seed_t seed, cuda::std::span segment_offsets, std::size_t elements); template void gen_power_law_segment_offsets_device(seed_t seed, cuda::std::span segment_offsets, std::size_t elements); namespace { struct generator_base_t { seed_t m_seed{}; const std::size_t m_elements{0}; const bit_entropy m_entropy{bit_entropy::_1_000}; template thrust::device_vector generate(T min, T max) { thrust::device_vector vec(m_elements); cuda::std::span span(thrust::raw_pointer_cast(vec.data()), m_elements); #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA gen_device(m_seed, span, m_entropy, min, max); #else gen_host(m_seed, span, m_entropy, min, max); #endif ++m_seed; return vec; } }; template struct vector_generator_t : generator_base_t { const T m_min{::cuda::std::numeric_limits::min()}; const T m_max{::cuda::std::numeric_limits::max()}; operator thrust::device_vector() { return generator_base_t::generate(m_min, m_max); } }; template <> struct vector_generator_t : generator_base_t { template operator thrust::device_vector() { return generator_base_t::generate(::cuda::std::numeric_limits::min(), ::cuda::std::numeric_limits::max()); } // This overload is needed because numeric limits is not specialized for complex, making // the min and max values for complex equal zero. template operator thrust::device_vector<::cuda::std::complex>() { const auto min = ::cuda::std::complex{::cuda::std::numeric_limits::min(), ::cuda::std::numeric_limits::min()}; const auto max = ::cuda::std::complex{::cuda::std::numeric_limits::max(), ::cuda::std::numeric_limits::max()}; return generator_base_t::generate(min, max); } }; struct uniform_key_segments_generator_t { seed_t m_seed{}; const std::size_t m_total_elements{0}; const std::size_t m_min_segment_size{0}; const std::size_t m_max_segment_size{0}; template operator thrust::device_vector() { thrust::device_vector keys_vec(m_total_elements); cuda::std::span keys(thrust::raw_pointer_cast(keys_vec.data()), keys_vec.size()); #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA gen_uniform_key_segments_device(m_seed, keys, m_min_segment_size, m_max_segment_size); #else gen_uniform_key_segments_host(m_seed, keys, m_min_segment_size, m_max_segment_size); #endif ++m_seed; return keys_vec; } }; struct uniform_segment_offsets_generator_t { seed_t m_seed{}; const std::size_t m_total_elements{0}; const std::size_t m_min_segment_size{0}; const std::size_t m_max_segment_size{0}; template operator thrust::device_vector() { thrust::device_vector offsets_vec(m_total_elements + 2); cuda::std::span offsets(thrust::raw_pointer_cast(offsets_vec.data()), offsets_vec.size()); const std::size_t offsets_size = #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA gen_uniform_segment_offsets_device(m_seed, offsets, m_min_segment_size, m_max_segment_size); #else gen_uniform_segment_offsets_host(m_seed, offsets, m_min_segment_size, m_max_segment_size); #endif offsets_vec.resize(offsets_size); offsets_vec.shrink_to_fit(); ++m_seed; return offsets_vec; } }; struct power_law_segment_offsets_generator_t { seed_t m_seed{}; const std::size_t m_elements{0}; const std::size_t m_segments{0}; template operator thrust::device_vector() { thrust::device_vector offsets_vec(m_segments + 1); cuda::std::span offsets(thrust::raw_pointer_cast(offsets_vec.data()), offsets_vec.size()); #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA gen_power_law_segment_offsets_device(m_seed, offsets, m_elements); #else gen_power_law_segment_offsets_host(m_seed, offsets, m_elements); #endif ++m_seed; return offsets_vec; } }; struct gen_uniform_key_segments_t { uniform_key_segments_generator_t operator()(std::size_t total_elements, std::size_t min_segment_size, std::size_t max_segment_size) const { return {seed_t{}, total_elements, min_segment_size, max_segment_size}; } }; struct gen_uniform_segment_offsets_t { uniform_segment_offsets_generator_t operator()(std::size_t total_elements, std::size_t min_segment_size, std::size_t max_segment_size) const { return {seed_t{}, total_elements, min_segment_size, max_segment_size}; } }; struct gen_uniform_t { gen_uniform_key_segments_t key_segments{}; gen_uniform_segment_offsets_t segment_offsets{}; }; struct gen_power_law_segment_offsets_t { power_law_segment_offsets_generator_t operator()(std::size_t elements, std::size_t segments) const { return {seed_t{}, elements, segments}; } }; struct gen_power_law_t { gen_power_law_segment_offsets_t segment_offsets{}; }; struct gen_t { vector_generator_t operator()(std::size_t elements, bit_entropy entropy = bit_entropy::_1_000) const { return {seed_t{}, elements, entropy}; } template vector_generator_t operator()( std::size_t elements, bit_entropy entropy = bit_entropy::_1_000, T min = ::cuda::std::numeric_limits::min, T max = ::cuda::std::numeric_limits::max()) const { return {{seed_t{}, elements, entropy}, min, max}; } gen_uniform_t uniform{}; gen_power_law_t power_law{}; }; } // namespace } // namespace detail inline detail::gen_t generate; template void do_not_optimize(const T& val) { detail::do_not_optimize(&val); } struct less_t { template __host__ __device__ bool operator()(const DataType& lhs, const DataType& rhs) const { return lhs < rhs; } template __host__ __device__ inline bool operator()(const ::cuda::std::complex& lhs, const ::cuda::std::complex& rhs) const { double magnitude_0 = cuda::std::abs(lhs); double magnitude_1 = cuda::std::abs(rhs); if (cuda::std::isnan(magnitude_0) || cuda::std::isnan(magnitude_1)) { // NaN's are always equal. return false; } else if (cuda::std::isinf(magnitude_0) || cuda::std::isinf(magnitude_1)) { // If the real or imaginary part of the complex number has a very large value // (close to the maximum representable value for a double), it is possible that // the magnitude computation can result in positive infinity: // ```cpp // const double large_number = ::cuda::std::numeric_limits::max() / 2; // std::complex z(large_number, large_number); // std::abs(z) == inf; // ``` // Dividing both components by a constant before computing the magnitude prevents overflow. const T scaler = 0.5; magnitude_0 = cuda::std::abs(lhs * scaler); magnitude_1 = cuda::std::abs(rhs * scaler); } const T difference = cuda::std::abs(magnitude_0 - magnitude_1); const T threshold = ::cuda::std::numeric_limits::epsilon() * 2; if (difference < threshold) { // Triangles with the same magnitude are sorted by their phase angle. const T phase_angle_0 = cuda::std::arg(lhs); const T phase_angle_1 = cuda::std::arg(rhs); return phase_angle_0 < phase_angle_1; } else { return magnitude_0 < magnitude_1; } } }; struct max_t { template __host__ __device__ DataType operator()(const DataType& lhs, const DataType& rhs) const { less_t less{}; return less(lhs, rhs) ? rhs : lhs; } #if _CCCL_HAS_NVFP16() && _CCCL_CTK_AT_LEAST(12, 2) __host__ __device__ __half operator()(__half lhs, __half rhs) const { return static_cast(lhs) < static_cast(rhs) ? rhs : lhs; } #endif // _CCCL_HAS_NVFP16() && _CCCL_CTK_AT_LEAST(12, 2) #if _CCCL_HAS_NVBF16() && _CCCL_CTK_AT_LEAST(12, 2) __host__ __device__ __nv_bfloat16 operator()(__nv_bfloat16 lhs, __nv_bfloat16 rhs) const { return static_cast(lhs) < static_cast(rhs) ? rhs : lhs; } #endif // _CCCL_HAS_NVBF16() && _CCCL_CTK_AT_LEAST(12, 2) }; template struct less_then_t { T m_val; [[nodiscard]] __device__ bool operator()(const T& val) const noexcept { return val < m_val; } }; _CCCL_BEGIN_NAMESPACE_CUDA template struct proclaims_copyable_arguments> : ::cuda::std::true_type {}; _CCCL_END_NAMESPACE_CUDA namespace { struct caching_allocator_t { using value_type = char; caching_allocator_t() = default; ~caching_allocator_t() { free_all(); } char* allocate(std::ptrdiff_t num_bytes) { #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA if (first_async_stream != ::cuda::invalid_stream) { // there was already an async allocate throw std::runtime_error("caching_allocator_t is not intended to be used asynchronously and synchronously at the " "same time"); } #endif // THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA value_type* result{}; auto free_block = free_blocks.find(num_bytes); if (free_block != free_blocks.end()) { result = free_block->second; free_blocks.erase(free_block); } else { result = do_allocate(num_bytes); } allocated_blocks.insert(std::make_pair(result, num_bytes)); return result; } void deallocate(char* ptr, size_t, [[maybe_unused]] bool check_stream = true) { #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA if (check_stream && first_async_stream != ::cuda::invalid_stream) { // there was already an async allocate throw std::runtime_error("caching_allocator_t is not intended to be used asynchronously and synchronously at the " "same time"); } #endif // THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA auto iter = allocated_blocks.find(ptr); if (iter == allocated_blocks.end()) { throw std::runtime_error("Memory was not allocated by this allocator"); } std::ptrdiff_t num_bytes = iter->second; allocated_blocks.erase(iter); free_blocks.insert(std::make_pair(num_bytes, ptr)); } #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA void* allocate_sync(size_t num_bytes, size_t) { return allocate(static_cast(num_bytes)); } void deallocate_sync(void* ptr, size_t num_bytes, size_t) { deallocate(static_cast(ptr), num_bytes); } void* allocate(::cuda::stream_ref __stream, size_t num_bytes, size_t) { if (first_async_stream == ::cuda::invalid_stream) { first_async_stream = __stream; } else if (first_async_stream != __stream) { throw std::runtime_error("caching_allocator_t is not intended to be used asynchronously from multiple streams"); } value_type* result{}; auto free_block = free_blocks.find(static_cast(num_bytes)); if (free_block != free_blocks.end()) { result = free_block->second; free_blocks.erase(free_block); } else { const cudaError_t status = cudaMallocAsync(&result, num_bytes, __stream.get()); if (cudaSuccess != status) { throw std::runtime_error(std::string("Failed to allocate device memory: ") + cudaGetErrorString(status)); } // NOTE: We can avoid a `__stream.sync()` here because `allocate` is only ever called asynchronously with a stream // So it is fine that the memory is only valid in stream order } allocated_blocks.insert(std::make_pair(result, num_bytes)); return result; } void deallocate(::cuda::stream_ref __stream, void* ptr, size_t num_bytes, size_t) { if (first_async_stream != __stream) { throw std::runtime_error("caching_allocator_t is not intended to be used asynchronously from multiple streams"); } // there is no need to sync the stream here and we can just insert the allocation into the free list, because the // next allocation can only be done from the same stream again. deallocate(static_cast(ptr), num_bytes, false); } #endif // THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA private: using free_blocks_type = std::multimap; using allocated_blocks_type = std::map; free_blocks_type free_blocks; allocated_blocks_type allocated_blocks; #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA ::cuda::stream_ref first_async_stream{::cuda::invalid_stream}; // just to detect wrong usage patterns #endif // THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA void free_all() { for (auto i : free_blocks) { do_deallocate(i.second); } for (auto i : allocated_blocks) { do_deallocate(i.first); } } value_type* do_allocate(std::size_t num_bytes) { value_type* result{}; #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA const cudaError_t status = cudaMalloc(&result, num_bytes); if (cudaSuccess != status) { throw std::runtime_error(std::string("Failed to allocate device memory: ") + cudaGetErrorString(status)); } #else result = new value_type[num_bytes]; #endif return result; } void do_deallocate(value_type* ptr) { #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA cudaFree(ptr); #else delete[] ptr; #endif } friend bool operator==(const caching_allocator_t& lhs, const caching_allocator_t& rhs) { return lhs.free_blocks == rhs.free_blocks && lhs.allocated_blocks == rhs.allocated_blocks; } friend bool operator!=(const caching_allocator_t& lhs, const caching_allocator_t& rhs) { return lhs.free_blocks == rhs.free_blocks && lhs.allocated_blocks == rhs.allocated_blocks; } #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA friend constexpr void get_property(const caching_allocator_t&, cuda::mr::device_accessible) noexcept {} #endif // THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA }; #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA auto policy(caching_allocator_t& alloc) { return thrust::cuda::par(alloc); } auto cuda_policy(caching_allocator_t& alloc) { return cuda::execution::gpu.with(cuda::mr::get_memory_resource, alloc); } #else auto policy(caching_allocator_t&) { return thrust::device; } #endif #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA auto policy(caching_allocator_t& alloc, nvbench::launch& launch) { return thrust::cuda::par(alloc).on(launch.get_stream()); } auto cuda_policy(caching_allocator_t& alloc, nvbench::launch& launch) { return cuda::execution::gpu.with(cuda::mr::get_memory_resource, alloc) .with(cuda::get_stream, launch.get_stream().get_stream()); } #else auto policy(caching_allocator_t&, nvbench::launch&) { return thrust::device; } #endif #if THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA // Returns an environment for benchmarking using alloc as MR, launch's stream, and any additional envs passed in. template auto cub_bench_env(caching_allocator_t& alloc, nvbench::launch& launch, MoreEnvs... envs) { return cuda::std::execution::env{ ::cuda::stream_ref{launch.get_stream().get_stream()}, ::cuda::std::execution::prop{cuda::mr::get_memory_resource, ::cuda::mr::resource_ref<>{alloc}}, envs...}; } #endif // THRUST_DEVICE_SYSTEM == THRUST_DEVICE_SYSTEM_CUDA } // namespace