// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // Block-level tests for `block_topk` / `block_topk_air`: deterministic // boundary fixtures (incl. `+/-0.0` ties) and both selection directions. #include #include #include #include #include #include #include #include "catch2_test_block_topk_common.cuh" #include namespace { template __global__ void topk_kernel(cuda::std::span g_in, cuda::std::span g_top, int k, int num_valid) { using topk_t = cub::detail::block_topk; __shared__ typename topk_t::TempStorage smem; KeyT keys[ItemsPerThread]; // Sentinel values are adversarial by design to surface bugs in partial tile handling. constexpr KeyT oob_sentinel = SelectMax ? cuda::std::numeric_limits::max() : cuda::std::numeric_limits::lowest(); if constexpr (BlockedInput) { cub::LoadDirectBlocked(static_cast(threadIdx.x), g_in.data(), keys, num_valid, oob_sentinel); } else { cub::LoadDirectStriped(static_cast(threadIdx.x), g_in.data(), keys, num_valid, oob_sentinel); } if constexpr (SelectMax) { topk_t(smem).template max_keys(keys, k, num_valid); } else { topk_t(smem).template min_keys(keys, k, num_valid); } for (int i = 0; i < ItemsPerThread; ++i) { const int idx = static_cast(threadIdx.x) * ItemsPerThread + i; if (idx < k) { g_top[idx] = keys[i]; } } } template void check_topk(const c2h::host_vector& h_in, cuda::std::span h_ref, int k) { constexpr int tile = BlockDim * ItemsPerThread; const int num_valid = static_cast(h_in.size()); REQUIRE(0 < k); REQUIRE(num_valid <= tile); REQUIRE((!IsFullTile || num_valid == tile)); const int top_size = cuda::std::min(k, num_valid); REQUIRE(static_cast(h_ref.size()) == top_size); c2h::device_vector d_in(h_in); c2h::device_vector d_top(k, KeyT{}); topk_kernel <<<1, BlockDim>>>(to_span(d_in), to_span(d_top), k, num_valid); REQUIRE(cudaSuccess == cudaPeekAtLastError()); REQUIRE(cudaSuccess == cudaDeviceSynchronize()); c2h::host_vector h_top(d_top); h_top.resize(top_size); std::sort(h_top.begin(), h_top.end(), direction_to_comparator_t{}); c2h::host_vector h_ref_vec(h_ref.begin(), h_ref.end()); CAPTURE(bit_repr(h_top), bit_repr(h_ref_vec)); REQUIRE(h_top == h_ref_vec); } } // namespace using select_direction_max = c2h::type_list; using fp_key_types = c2h::type_list; template struct block_shape { static constexpr int threads_per_block = BlockDim; static constexpr int items_per_thread = ItemsPerThread; }; using block_shapes_full_tile = c2h::type_list, block_shape<256, 2>, block_shape<32, 16>, block_shape<128, 4>>; C2H_TEST("block_topk preserves keys across FP edge cases", "[block][topk]", fp_key_types, select_direction_max) { using key_t = c2h::get<0, TestType>; static constexpr bool select_max = c2h::get<1, TestType>::value; static constexpr int threads_per_block = 128; static constexpr int items_per_thread = 4; static constexpr int tile_size = threads_per_block * items_per_thread; rng_t rng(static_cast(C2H_SEED(1).get())); c2h::host_vector h_in = distinct_keys(tile_size, rng); h_in[0] = static_cast(-0.0); h_in[1] = static_cast(+0.0); h_in[2] = cuda::std::numeric_limits::infinity(); h_in[3] = -cuda::std::numeric_limits::infinity(); thrust::shuffle(h_in.begin(), h_in.end(), rng); CAPTURE(c2h::type_name(), select_max); const int num_valid = tile_size / 2 + 7; c2h::host_vector h_in_partial(h_in.begin(), h_in.begin() + num_valid); SECTION("full tile, blocked input") { static constexpr bool is_full_tile = true; static constexpr bool blocked_input = true; const int k = GENERATE_COPY(values({1, tile_size / 4, tile_size - 1})); CAPTURE(k); const auto h_ref = sorted_top_k(h_in, k); check_topk( h_in, to_span(h_ref), k); } SECTION("partial tile, blocked input") { static constexpr bool is_full_tile = false; static constexpr bool blocked_input = true; const int k = GENERATE_COPY(values({1, num_valid / 4, num_valid - 1})); CAPTURE(num_valid, k); const auto h_ref = sorted_top_k(h_in_partial, k); check_topk( h_in_partial, to_span(h_ref), k); } } C2H_TEST("block_topk::select_* selects the right top-k on a full tile", "[block][topk]", fp_key_types, block_shapes_full_tile, select_direction_max) { using key_t = c2h::get<0, TestType>; using shape_t = c2h::get<1, TestType>; static constexpr bool select_max = c2h::get<2, TestType>::value; static constexpr int threads_per_block = shape_t::threads_per_block; static constexpr int items_per_thread = shape_t::items_per_thread; static constexpr int tile_size = threads_per_block * items_per_thread; rng_t rng(static_cast(C2H_SEED(2).get())); const int k = GENERATE_COPY(values({1, tile_size / 4, tile_size / 2, tile_size - 1})); static constexpr bool is_full_tile = true; static constexpr bool blocked_input = true; const int overhang = GENERATE_COPY(overhang_generator(tile_size - k <= 1, {0, 1, tile_size - k})); auto run_check = [&](rng_t& local_rng, key_t boundary_key) { CAPTURE(c2h::type_name(), select_max, k, overhang, boundary_key); c2h::host_vector h_in = gen_keys_from_boundary_key(tile_size, k, overhang, boundary_key, local_rng); const auto h_ref = sorted_top_k(h_in, k); check_topk( h_in, to_span(h_ref), k); }; SECTION("fixed boundary_key") { const key_t boundary_key = GENERATE_COPY(boundary_key_generator()); run_check(rng, boundary_key); } SECTION("random boundary_key") { const key_t boundary_key = random_boundary_key(rng); run_check(rng, boundary_key); } } C2H_TEST("block_topk::{Min,Max}Keys preserve -0.0 in output", "[block][topk][float]", select_direction_max) { static constexpr bool select_max = c2h::get<0, TestType>::value; static constexpr int threads_per_block = 128; static constexpr int items_per_thread = 1; static constexpr int tile_size = 8; const c2h::host_vector h_in = select_max ? c2h::host_vector{-2.0f, -0.0f, -3.0f, 0.0f, -1.0f, -4.0f, -5.0f, -6.0f} : c2h::host_vector{3.0f, -0.0f, 1.0f, 2.0f, 0.0f, -1.0f, 4.0f, 5.0f}; const int k = select_max ? 3 : 5; const auto h_ref = sorted_top_k(h_in, k); check_topk(h_in, to_span(h_ref), k); c2h::device_vector d_in(h_in); c2h::device_vector d_top(k); topk_kernel <<<1, threads_per_block>>>(to_span(d_in), to_span(d_top), k, tile_size); REQUIRE(cudaSuccess == cudaDeviceSynchronize()); c2h::host_vector h_top(d_top); const int num_minus_zero = static_cast(thrust::count_if(h_top.begin(), h_top.end(), [](float x) { return x == 0.0f && std::signbit(x); })); REQUIRE(num_minus_zero >= 1); }