// SPDX-FileCopyrightText: Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved. // SPDX-License-Identifier: BSD-3 #include #include #include #include #include #include #include template struct fundamental_extractor_t { std::uint32_t bit_start; std::uint32_t mask; // NOLINT(modernize-use-default-member-init) __host__ __device__ fundamental_extractor_t(std::uint32_t bit_start = 0, std::uint32_t num_bits = 0) : bit_start(bit_start) , mask((1 << num_bits) - 1) {} __host__ __device__ std::uint32_t Digit(KeyT key) const { return std::uint32_t(key >> KeyT(bit_start)) & mask; } }; template c2h::host_vector get_random_buffer() { c2h::device_vector buffer(sizeof(T)); c2h::gen(C2H_SEED(3), buffer); return buffer; } constexpr int max_digit_bits = sizeof(std::uint32_t) * CHAR_BIT; using digit_bits_t = std::bitset; digit_bits_t buffer_to_digit_bits(const char* buffer, int current_bit, int num_bits) { digit_bits_t dst; // all bits set to zero for (int bit = current_bit; bit < current_bit + num_bits; bit++) { const int dst_bit = bit - current_bit; const int src_byte = bit / CHAR_BIT; const int src_bit = bit % CHAR_BIT; std::bitset src(buffer[src_byte]); dst[dst_bit] = src[src_bit]; } return dst; } using fundamental_types = c2h::type_list; using a_few_fundamental_types = c2h::type_list; /** * This test checks that radix operations can extract certain bits out of unsigned integers. * Test runs for all possible combinations of `current_bit` and `num_bits`. * Example for `current_bit = 5`, and `num_bits = 4`: * * [-------] * src: 1 1 0 0 1 1 0 0 1 1 * bit: 9 8 7 6 5 4 3 2 1 0 * dst: 0 0 0 0 0 0 1 0 0 1 * */ C2H_TEST("Radix operations extract digits from fundamental types", "[radix][operations]", fundamental_types) { using key_t = typename c2h::get<0, TestType>; using traits = cub::detail::radix::traits_t; using extractor_t = fundamental_extractor_t; using decomposer_t = cub::detail::identity_decomposer_t; auto decomposer = decomposer_t{}; constexpr int max_key_bits = sizeof(key_t) * CHAR_BIT; REQUIRE(traits::default_end_bit(decomposer) == max_key_bits); key_t val{}; c2h::host_vector output_buffer_mem(sizeof(std::uint32_t)); const c2h::host_vector input_buffer_mem = get_random_buffer(); char* output_buffer = thrust::raw_pointer_cast(output_buffer_mem.data()); const char* input_buffer = thrust::raw_pointer_cast(input_buffer_mem.data()); std::memcpy(&val, input_buffer, sizeof(key_t)); for (int current_bit = 0; current_bit < max_key_bits; current_bit++) { const int max_bits = std::min(max_key_bits - current_bit, max_digit_bits); for (int num_bits = 1; num_bits < max_bits; num_bits++) { auto extractor = traits::template digit_extractor(current_bit, num_bits, decomposer); std::uint32_t digit = extractor.Digit(val); std::memcpy(output_buffer, &digit, sizeof(std::uint32_t)); digit_bits_t result = buffer_to_digit_bits(output_buffer, 0, num_bits); digit_bits_t reference = buffer_to_digit_bits(input_buffer, current_bit, num_bits); REQUIRE(reference == result); } } } template struct tuple_decomposer_t; template struct tuple_decomposer_t> { template __host__ __device__ cuda::std::tuple extract(cuda::std::tuple& key, cuda::std::index_sequence) const { return cuda::std::tie(cuda::std::get(key)...); } __host__ __device__ cuda::std::tuple operator()(cuda::std::tuple& key) const { return extract(key, cuda::std::make_index_sequence{}); } }; // clang-format off template cuda::std::enable_if_t buffer_to_tpl_helper(const char *buffer, cuda::std::tuple &tpl) { constexpr std::size_t element_size = sizeof(cuda::std::tuple_element_t>); std::memcpy(&cuda::std::get(tpl), buffer, element_size); } template cuda::std::enable_if_t buffer_to_tpl_helper(const char *buffer, cuda::std::tuple &tpl) { constexpr std::size_t element_size = sizeof(cuda::std::tuple_element_t>); std::memcpy(&cuda::std::get(tpl), buffer, element_size); buffer_to_tpl_helper(buffer + element_size, tpl); } template void buffer_to_tpl(const char *buffer, cuda::std::tuple &tpl) { buffer_to_tpl_helper(buffer, tpl); } template cuda::std::enable_if_t tpl_to_buffer_helper(char *buffer, cuda::std::tuple &tpl) { constexpr std::size_t element_size = sizeof(cuda::std::tuple_element_t>); std::memcpy(buffer, &cuda::std::get(tpl), element_size); } template cuda::std::enable_if_t tpl_to_buffer_helper(char *buffer, cuda::std::tuple &tpl) { constexpr std::size_t element_size = sizeof(cuda::std::tuple_element_t>); std::memcpy(buffer, &cuda::std::get(tpl), element_size); tpl_to_buffer_helper(buffer + element_size, tpl); } template void tpl_to_buffer(char *buffer, cuda::std::tuple &tpl) { tpl_to_buffer_helper(buffer, tpl); } template cuda::std::enable_if_t= sizeof...(Ts), int> tpl_to_max_bits(cuda::std::tuple &) { return 0; } template cuda::std::enable_if_t tpl_to_max_bits(cuda::std::tuple &tpl) { constexpr std::size_t element_size = sizeof(cuda::std::tuple_element_t>); return element_size * CHAR_BIT + tpl_to_max_bits(tpl); } template cuda::std::enable_if_t= sizeof...(Ts)> tpl_to_min(cuda::std::tuple &) {} template cuda::std::enable_if_t tpl_to_min(cuda::std::tuple &tpl) { using T = cuda::std::tuple_element_t>; cuda::std::get(tpl) = cuda::std::numeric_limits::lowest(); tpl_to_min(tpl); } template cuda::std::enable_if_t= sizeof...(Ts)> tpl_to_max(cuda::std::tuple &) {} template cuda::std::enable_if_t tpl_to_max(cuda::std::tuple &tpl) { using T = cuda::std::tuple_element_t>; cuda::std::get(tpl) = cuda::std::numeric_limits::max(); tpl_to_max(tpl); } // clang-format on /** * This test checks that radix operations can extract certain bits out of aggregate types. * Test runs for all possible combinations of `current_bit` and `num_bits` excluding padding bits. * For example, `struct custom_t { short s = 65535; float f = -42.2f; };` has the following binary * representation: * * <------------ `.f` ------------><-- padding ---><---- `.s` ----> * s< exp. ><----- mantissa ------><-- padding ---><--- short ----> * 1100000010000110011001100110011000000000000000001111111111111111 * +---~ ~--+ * < <---- higher bits / lower bits ----> > * * For `current_bit = 12`, and `num_bits = 9`: * dst: 0000011011111 * < fp > * */ template void test_tuple() { using tpl_t = cuda::std::tuple; using traits = cub::detail::radix::traits_t; using decomposer_t = tuple_decomposer_t; using extractor_t = cub::detail::radix::custom_digit_extractor_t; tpl_t tpl{}; c2h::host_vector output_buffer_mem(sizeof(std::uint32_t)); const c2h::host_vector input_buffer_mem = get_random_buffer(); char* output_buffer = thrust::raw_pointer_cast(output_buffer_mem.data()); const char* input_buffer = thrust::raw_pointer_cast(input_buffer_mem.data()); buffer_to_tpl(input_buffer, tpl); auto decomposer = decomposer_t{}; const int max_key_bits = tpl_to_max_bits(tpl); REQUIRE(traits::default_end_bit(decomposer) == max_key_bits); for (int current_bit = 0; current_bit < max_key_bits; current_bit++) { const int max_bits = std::min(max_key_bits - current_bit, max_digit_bits); for (int num_bits = 1; num_bits < max_bits; num_bits++) { auto extractor = traits::template digit_extractor(current_bit, num_bits, decomposer); std::uint32_t digit = extractor.Digit(tpl); std::memcpy(output_buffer, &digit, sizeof(std::uint32_t)); digit_bits_t result = buffer_to_digit_bits(output_buffer, 0, num_bits); digit_bits_t reference = buffer_to_digit_bits(input_buffer, current_bit, num_bits); // Provides readable error messages: // 00000000000000000000000000000000 // == // 00000000000000000000000000000001 REQUIRE(reference == result); } } } C2H_TEST("Radix operations extract digits from pairs", "[radix][operations]", fundamental_types, fundamental_types) { test_tuple, // typename c2h::get<1, TestType>>(); } C2H_TEST("Radix operations extract digits from triples", "[radix][operations]", fundamental_types, fundamental_types, fundamental_types) { test_tuple, // typename c2h::get<1, TestType>, // typename c2h::get<2, TestType>>(); } C2H_TEST("Radix operations extract digits from tetrads", "[radix][operations]", a_few_fundamental_types, a_few_fundamental_types, a_few_fundamental_types, a_few_fundamental_types) { test_tuple, // typename c2h::get<1, TestType>, // typename c2h::get<2, TestType>, // typename c2h::get<3, TestType>>(); } /** * This test checks that radix operations can invert bits (`~`) of fundamental types. * * src: 1 1 0 0 1 1 0 0 1 1 * dst: 0 0 1 1 0 0 1 1 0 0 * */ C2H_TEST("Radix operations inverse fundamental types", "[radix][operations]", fundamental_types) { using key_t = typename c2h::get<0, TestType>; using traits = cub::detail::radix::traits_t; using extractor_t = fundamental_extractor_t; using decomposer_t = cub::detail::identity_decomposer_t; auto decomposer = decomposer_t{}; key_t val{}; c2h::host_vector output_buffer_mem(sizeof(key_t)); c2h::host_vector input_buffer_mem = get_random_buffer(); char* output_buffer = thrust::raw_pointer_cast(output_buffer_mem.data()); char* input_buffer = thrust::raw_pointer_cast(input_buffer_mem.data()); std::memcpy(&val, input_buffer, sizeof(key_t)); for (std::size_t i = 0; i < input_buffer_mem.size(); i++) { input_buffer[i] = static_cast(~input_buffer[i]); } key_t inv = traits::bit_ordered_inversion_policy::inverse(decomposer, val); std::memcpy(output_buffer, &inv, sizeof(key_t)); REQUIRE(input_buffer_mem == output_buffer_mem); } /** * This test checks that radix operations can invert bits (`~`) of aggregate types. * For example, `struct custom_t { short s = 65535; float f = -42.2f; };`: * * <------------ `.f` ------------><-- padding ---><---- `.s` ----> * s< exp. ><----- mantissa ------><-- padding ---><--- short ----> * src: 1100000010000110011001100110011000000000000000001111111111111111 * +------------------------------~ ~--------------+ * dst: 0011111101111001100110011001100111111111111111110000000000000000 * < <---- higher bits / lower bits ----> > * */ C2H_TEST("Radix operations inverse pairs", "[radix][operations]", fundamental_types, fundamental_types) { using tpl_t = cuda::std::tuple, // typename c2h::get<1, TestType>>; using traits = cub::detail::radix::traits_t; using decomposer_t = tuple_decomposer_t; using extractor_t = cub::detail::radix::custom_digit_extractor_t; auto decomposer = decomposer_t{}; tpl_t tpl{}; c2h::host_vector input_buffer_mem = get_random_buffer(); char* input_buffer = thrust::raw_pointer_cast(input_buffer_mem.data()); buffer_to_tpl(input_buffer, tpl); for (std::size_t i = 0; i < input_buffer_mem.size(); i++) { input_buffer[i] = static_cast(~input_buffer[i]); } c2h::host_vector output_buffer_mem = input_buffer_mem; char* output_buffer = thrust::raw_pointer_cast(output_buffer_mem.data()); tpl_t inv = traits::bit_ordered_inversion_policy::inverse(decomposer, tpl); tpl_to_buffer(output_buffer, inv); REQUIRE(input_buffer_mem == output_buffer_mem); } /** * This tests checks that radix operations can get a value that when converted * to binary-comparable representation, yields smallest possible value. */ C2H_TEST("Radix operations infere minimal value for fundamental types", "[radix][operations]", fundamental_types) { using key_t = typename c2h::get<0, TestType>; using traits = cub::detail::radix::traits_t; using decomposer_t = cub::detail::identity_decomposer_t; c2h::host_vector output_buffer_mem(sizeof(key_t)); c2h::host_vector input_buffer_mem(sizeof(key_t)); key_t ref = cuda::std::numeric_limits::lowest(); key_t val = traits::min_raw_binary_key(decomposer_t{}); REQUIRE(ref == val); } C2H_TEST( "Radix operations infere minimal value for pair types", "[radix][operations]", fundamental_types, fundamental_types) { using tpl_t = cuda::std::tuple, // typename c2h::get<1, TestType>>; using traits = cub::detail::radix::traits_t; using decomposer_t = tuple_decomposer_t; tpl_t ref; tpl_to_min(ref); tpl_t val = traits::min_raw_binary_key(decomposer_t{}); REQUIRE(ref == val); } /** * This tests checks that radix operations can get a value that when converted * to binary-comparable representation, yields largest possible value. */ C2H_TEST("Radix operations infere maximal value for fundamental types", "[radix][operations]", fundamental_types) { using key_t = typename c2h::get<0, TestType>; using traits = cub::detail::radix::traits_t; using decomposer_t = cub::detail::identity_decomposer_t; key_t ref = cuda::std::numeric_limits::max(); key_t val = traits::max_raw_binary_key(decomposer_t{}); REQUIRE(ref == val); } C2H_TEST( "Radix operations infere maximal value for pair types", "[radix][operations]", fundamental_types, fundamental_types) { using tpl_t = cuda::std::tuple, // typename c2h::get<1, TestType>>; using traits = cub::detail::radix::traits_t; using decomposer_t = tuple_decomposer_t; tpl_t ref; tpl_to_max(ref); tpl_t val = traits::max_raw_binary_key(decomposer_t{}); REQUIRE(ref == val); } using fundamental_signed_types = c2h::type_list; /** * This tests checks that radix operations can convert a value to a binary-comparable * representation. For example, `42.0f` is larger than `-42.0f`, but if we look at the * binary representation, it's not the case because of the sign bit: * * s< exp. ><----- mantissa ------> * 42.0f: 01000010001010000000000000000000 * -42.0f: 11000010001010000000000000000000 * */ C2H_TEST("Radix operations reorder values for pair types", "[radix][operations]", fundamental_signed_types, fundamental_signed_types) { using T1 = typename c2h::get<0, TestType>; using UT1 = std::make_unsigned_t; using T2 = typename c2h::get<1, TestType>; using UT2 = std::make_unsigned_t; using tpl_t = cuda::std::tuple; using traits = cub::detail::radix::traits_t; using conversion_policy = typename traits::bit_ordered_conversion_policy; using decomposer_t = tuple_decomposer_t; std::bitset bs_1; std::bitset bs_2; // 10000(0) bs_1.set(sizeof(T1) * CHAR_BIT - 1); bs_2.set(sizeof(T2) * CHAR_BIT - 1); UT1 ul_1 = static_cast(bs_1.to_ullong()); UT2 ul_2 = static_cast(bs_2.to_ullong()); T1 l_1 = reinterpret_cast(ul_1); T2 l_2 = reinterpret_cast(ul_2); REQUIRE(l_1 == cuda::std::numeric_limits::lowest()); REQUIRE(l_2 == cuda::std::numeric_limits::lowest()); { tpl_t ref{T1{0}, T2{0}}; const tpl_t unordered_val = tpl_t{l_1, l_2}; const tpl_t ordered_val = conversion_policy::to_bit_ordered(decomposer_t{}, unordered_val); REQUIRE(ref == ordered_val); const tpl_t restored_val = conversion_policy::from_bit_ordered(decomposer_t{}, ordered_val); REQUIRE(restored_val == unordered_val); } ul_1 = static_cast(cuda::std::numeric_limits::max()); ul_2 = static_cast(cuda::std::numeric_limits::max()); l_1 = reinterpret_cast(ul_1); l_2 = reinterpret_cast(ul_2); bs_1 = ul_1; bs_2 = ul_2; REQUIRE_FALSE(bs_1[sizeof(T1) * CHAR_BIT - 1]); REQUIRE_FALSE(bs_2[sizeof(T2) * CHAR_BIT - 1]); { const tpl_t unordered_val = tpl_t{l_1, l_2}; const tpl_t ordered_val = conversion_policy::to_bit_ordered(decomposer_t{}, unordered_val); ul_1 = reinterpret_cast(cuda::std::get<0>(ordered_val)); ul_2 = reinterpret_cast(cuda::std::get<1>(ordered_val)); REQUIRE(ul_1 == cuda::std::numeric_limits::max()); REQUIRE(ul_2 == cuda::std::numeric_limits::max()); const tpl_t restored_val = conversion_policy::from_bit_ordered(decomposer_t{}, ordered_val); REQUIRE(restored_val == unordered_val); } } struct fp_aggregate_t { double fp64; float fp32; }; struct fp_aggregate_decomposer_t { __host__ __device__ cuda::std::tuple operator()(fp_aggregate_t& val) const { return {val.fp64, val.fp32}; } }; struct flipped_fp_aggregate_decomposer_t { __host__ __device__ cuda::std::tuple operator()(fp_aggregate_t& val) const { return {val.fp32, val.fp64}; } }; /** * This tests checks radix sort guarantees to treat +0/-0 as the same value. */ TEST_CASE("Radix operations treat -0/+0 as being equal", "[radix][operations]") { using traits = cub::detail::radix::traits_t; using conversion_policy = typename traits::bit_ordered_conversion_policy; using decomposer_t = fp_aggregate_decomposer_t; using extractor_t = cub::detail::radix::custom_digit_extractor_t; fp_aggregate_t negative{-0.0, -0.0f}; fp_aggregate_t positive{+0.0, +0.0f}; fp_aggregate_t ordered_negative = conversion_policy::to_bit_ordered(decomposer_t{}, negative); fp_aggregate_t ordered_positibe = conversion_policy::to_bit_ordered(decomposer_t{}, positive); constexpr int num_bits = CHAR_BIT; for (int bit = 0; bit < 8; bit += num_bits) { auto extractor = traits::digit_extractor(bit, num_bits, decomposer_t{}); const std::uint32_t digit_positive = extractor.Digit(ordered_positibe); const std::uint32_t digit_negative = extractor.Digit(ordered_negative); REQUIRE(digit_positive == digit_negative); } } /** * This tests checks that radix operations respect the order of fields in the * tuple instead of looking at the binary key representation. */ TEST_CASE("Radix operations allow fields permutation", "[radix][operations]") { using traits = cub::detail::radix::traits_t; using conversion_policy = typename traits::bit_ordered_conversion_policy; using decomposer_t = flipped_fp_aggregate_decomposer_t; using extractor_t = cub::detail::radix::custom_digit_extractor_t; fp_aggregate_t lhs{4.2, 2.4f}; fp_aggregate_t rhs{2.4, 4.2f}; REQUIRE(cuda::std::tie(lhs.fp64, lhs.fp32) > cuda::std::tie(rhs.fp64, rhs.fp32)); fp_aggregate_t ordered_lhs = conversion_policy::to_bit_ordered(decomposer_t{}, lhs); fp_aggregate_t ordered_rhs = conversion_policy::to_bit_ordered(decomposer_t{}, lhs); constexpr int num_bits = CHAR_BIT; constexpr int aggregate_bits = (sizeof(float) + sizeof(double)) * CHAR_BIT; for (int current_bit = aggregate_bits - num_bits; current_bit >= 0; current_bit -= num_bits) { auto extractor = traits::digit_extractor(current_bit, num_bits, decomposer_t{}); const std::uint32_t digit_lhs = extractor.Digit(ordered_lhs); const std::uint32_t digit_rhs = extractor.Digit(ordered_rhs); if (digit_lhs == digit_rhs) { continue; } std::bitset<32> bs_lhs(digit_lhs); std::bitset<32> bs_rhs(digit_rhs); for (int bit = 31; bit >= 0; bit--) { REQUIRE_FALSE(bs_lhs[bit]); if (bs_rhs[bit]) { return; } } } }