//===----------------------------------------------------------------------===// // // Part of CUDA Experimental in CUDA C++ Core Libraries, // under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. // //===----------------------------------------------------------------------===// #include #include #include #include #include "algorithm_execution.h" #include "test_util.h" #include using sample_types = c2h::type_list; constexpr int num_channels = 1; constexpr int num_active_channels = 1; void build_histogram( cccl_device_histogram_build_result_t* build, cccl_iterator_t d_samples, int num_output_levels_val, cccl_iterator_t d_output_histograms, cccl_type_info level_type, uint64_t num_rows, uint64_t row_stride_samples, bool is_evenly_segmented) { cudaDeviceProp deviceProp; cudaGetDeviceProperties(&deviceProp, 0); const int cc_major = deviceProp.major; const int cc_minor = deviceProp.minor; const char* cub_path = TEST_CUB_PATH; const char* thrust_path = TEST_THRUST_PATH; const char* libcudacxx_path = TEST_LIBCUDACXX_PATH; const char* ctk_path = TEST_CTK_PATH; REQUIRE( CUDA_SUCCESS == cccl_device_histogram_build( build, num_channels, num_active_channels, d_samples, num_output_levels_val, d_output_histograms, level_type, num_rows, row_stride_samples, is_evenly_segmented, cc_major, cc_minor, cub_path, thrust_path, libcudacxx_path, ctk_path)); } void histogram_even( cccl_iterator_t d_samples, cccl_iterator_t d_output_histograms, cccl_value_t num_output_levels, int num_output_levels_val, cccl_value_t lower_level, cccl_value_t upper_level, int64_t num_row_pixels, int64_t num_rows, int64_t row_stride_samples) { cccl_device_histogram_build_result_t build{}; build_histogram( &build, d_samples, num_output_levels_val, d_output_histograms, lower_level.type, num_rows, row_stride_samples, true); size_t temp_storage_bytes = 0; REQUIRE( CUDA_SUCCESS == cccl_device_histogram_even( build, nullptr, &temp_storage_bytes, d_samples, d_output_histograms, num_output_levels, lower_level, upper_level, num_row_pixels, num_rows, row_stride_samples, nullptr)); pointer_t temp_storage(temp_storage_bytes); REQUIRE( CUDA_SUCCESS == cccl_device_histogram_even( build, temp_storage.ptr, &temp_storage_bytes, d_samples, d_output_histograms, num_output_levels, lower_level, upper_level, num_row_pixels, num_rows, row_stride_samples, nullptr)); REQUIRE(CUDA_SUCCESS == cccl_device_histogram_cleanup(&build)); } // Copied from catch2_test_device_histogram.cu (With some modifications) template auto generate_level_counts_to_test(int max_level_count) -> std::vector { // first channel tests maximum number of levels, later channels less and less std::vector r{max_level_count}; for (size_t c = 1; c < ActiveChannels; ++c) { r[c] = r[c - 1] / 2 + 1; } return r; } template auto setup_bin_levels_for_even(const std::vector& num_levels, LevelT max_level, int max_level_count) -> std::vector> { std::vector> levels(2); auto& lower_level = levels[0]; auto& upper_level = levels[1]; lower_level.resize(ActiveChannels); upper_level.resize(ActiveChannels); // Create upper and lower levels between between [0:max_level], getting narrower with each channel. Example: // max_level = 256 // num_levels = { 257, 129, 65 } // lower_level = { 0, 64, 96 } // upper_level = { 256, 192, 160 } const auto min_bin_width = max_level / (max_level_count - 1); REQUIRE(min_bin_width > 0); for (size_t c = 0; c < ActiveChannels; ++c) { const int num_bins = num_levels[c] - 1; const auto min_hist_width = num_bins * min_bin_width; lower_level[c] = static_cast(max_level / 2 - min_hist_width / 2); upper_level[c] = static_cast(max_level / 2 + min_hist_width / 2); REQUIRE(lower_level[c] < upper_level[c]); } return levels; } template auto compute_reference_result( const std::vector& h_samples, const TransformOp& sample_to_bin_index, const std::vector& num_levels, OffsetT width, OffsetT height, OffsetT row_pitch) -> std::array, ActiveChannels> { auto h_histogram = std::array, ActiveChannels>{}; for (size_t c = 0; c < ActiveChannels; ++c) { h_histogram[c].resize(num_levels[c] - 1); } for (OffsetT row = 0; row < height; ++row) { for (OffsetT pixel = 0; pixel < width; ++pixel) { for (size_t c = 0; c < ActiveChannels; ++c) { const auto offset = row * (row_pitch / sizeof(SampleT)) + pixel * Channels + c; const int bin = sample_to_bin_index(static_cast(c), h_samples[offset]); if (bin >= 0 && bin < static_cast(h_histogram[c].size())) // if bin is valid { ++h_histogram[c][bin]; } } } } return h_histogram; } C2H_TEST("DeviceHistogram::HistogramEven API usage", "[histogram][device]") { using counter_t = int; using level_t = float; int num_samples = 10; std::vector d_samples{2.2f, 6.1f, 7.1f, 2.9f, 3.5f, 0.3f, 2.9f, 2.1f, 6.1f, 999.5f}; int num_rows = 1; int num_levels = 7; std::vector d_num_levels{num_levels}; std::vector d_single_histogram(6, 0); pointer_t d_single_histogram_ptr(d_single_histogram); level_t lower_level = 0.0; level_t upper_level = 12.0; pointer_t d_samples_ptr(d_samples); value_t num_levels_val{num_levels}; pointer_t d_num_levels_ptr(d_num_levels); value_t lower_level_val{lower_level}; value_t upper_level_val{upper_level}; int64_t row_stride_samples = static_cast(num_samples); histogram_even( d_samples_ptr, d_single_histogram_ptr, num_levels_val, num_levels, lower_level_val, upper_level_val, num_samples, num_rows, row_stride_samples); std::vector d_histogram_out(d_single_histogram_ptr); CHECK(d_histogram_out == std::vector{1, 5, 0, 3, 0, 0}); } C2H_TEST("DeviceHistogram::HistogramEven basic use", "[histogram][device]", sample_types) { using counter_t = int; using sample_t = c2h::get<0, TestType>; using offset_t = int; using level_t = std::conditional_t, sample_t, int>; const auto max_level = level_t{sizeof(sample_t) == 1 ? 126 : 1024}; const auto max_level_count = (sizeof(sample_t) == 1 ? 126 : 1024) + 1; offset_t width = 1920; offset_t height = 1080; constexpr int channels = 1; constexpr int active_channels = 1; const auto padding_bytes = static_cast(GENERATE(size_t{0}, 13 * sizeof(sample_t))); const offset_t row_pitch = width * channels * sizeof(sample_t) + padding_bytes; const auto num_levels = generate_level_counts_to_test(max_level_count); const offset_t total_samples = height * (row_pitch / sizeof(sample_t)); std::vector samples_gen = generate(total_samples); std::vector h_samples(total_samples); for (int i = 0; i < total_samples; i++) { h_samples[i] = static_cast(samples_gen[i]); } std::vector d_single_histogram(num_levels[0] - 1, 0); auto levels = setup_bin_levels_for_even(num_levels, max_level, max_level_count); auto& lower_level = levels[0]; auto& upper_level = levels[1]; // Compute reference result auto fp_scales = ::cuda::std::array{}; // only used when LevelT is floating point for (size_t c = 0; c < active_channels; ++c) { if constexpr (!std::is_integral_v) { fp_scales[c] = static_cast(num_levels[c] - 1) / static_cast(upper_level[c] - lower_level[c]); } } auto sample_to_bin_index = [&](int channel, sample_t sample) { using common_t = ::cuda::std::common_type_t; const auto n = num_levels[channel]; const auto max = static_cast(upper_level[channel]); const auto min = static_cast(lower_level[channel]); const auto promoted_sample = static_cast(sample); if (promoted_sample < min || promoted_sample >= max) { return n; // out of range } if constexpr (::cuda::std::is_integral::value) { // Accurate bin computation following the arithmetic we guarantee in the HistoEven docs return static_cast( static_cast(promoted_sample - min) * static_cast(n - 1) / static_cast(max - min)); } else { return static_cast((static_cast(sample) - min) * fp_scales[channel]); } _CCCL_UNREACHABLE(); }; auto h_histogram = compute_reference_result( h_samples, sample_to_bin_index, num_levels, width, height, row_pitch); // Compute result and verify pointer_t sample_ptr(h_samples); pointer_t d_single_histogram_ptr(d_single_histogram); value_t num_levels_val{num_levels[0]}; value_t lower_level_val{lower_level[0]}; value_t upper_level_val{upper_level[0]}; histogram_even( sample_ptr, d_single_histogram_ptr, num_levels_val, num_levels[0], lower_level_val, upper_level_val, width, height, row_pitch / sizeof(sample_t)); for (size_t c = 0; c < active_channels; ++c) { CHECK(h_histogram[c] == std::vector(d_single_histogram_ptr)); } } C2H_TEST("DeviceHistogram::HistogramEven sample iterator", "[histogram][device]") { using counter_t = int; using sample_t = std::int32_t; using offset_t = int; using level_t = int; const auto max_level_count = 1025; const auto num_levels = generate_level_counts_to_test(max_level_count); const int num_bins = num_levels[0] - 1; const offset_t samples_per_bin = 10; const offset_t adjusted_total_samples = num_bins * samples_per_bin; // Set up iterator that counts from 0 to adjusted_total_samples - 1 iterator_t> counting_it = make_counting_iterator("int"); counting_it.state.value = static_cast(0); std::vector d_single_histogram(num_levels[0] - 1, 0); // Set up levels so that values 0 to adjusted_total_samples-1 are evenly distributed std::vector> levels(2); auto& lower_level = levels[0]; auto& upper_level = levels[1]; lower_level.resize(num_active_channels); upper_level.resize(num_active_channels); lower_level[0] = static_cast(0); upper_level[0] = static_cast(adjusted_total_samples); // Compute reference result - each bin should have exactly samples_per_bin elements auto h_histogram = std::array, num_active_channels>{}; h_histogram[0].resize(num_levels[0] - 1, samples_per_bin); // Compute result and verify pointer_t d_single_histogram_ptr(d_single_histogram); value_t num_levels_val{num_levels[0]}; value_t lower_level_val{lower_level[0]}; value_t upper_level_val{upper_level[0]}; histogram_even( counting_it, d_single_histogram_ptr, num_levels_val, num_levels[0], lower_level_val, upper_level_val, adjusted_total_samples, 1, adjusted_total_samples); for (size_t c = 0; c < num_active_channels; ++c) { CHECK(h_histogram[c] == std::vector(d_single_histogram_ptr)); } } #ifndef CCCL_C_PARALLEL_V2 C2H_TEST("Histogram build result has serialization metadata populated", "[histogram][device][serialization]") { using T = int32_t; constexpr int device_id = 0; const auto& build_info = BuildInformation::init(); pointer_t samples(1); pointer_t histograms(1); cccl_device_histogram_build_result_t build{}; REQUIRE( CUDA_SUCCESS == cccl_device_histogram_build( &build, /*num_channels=*/1, /*num_active_channels=*/1, samples, /*num_output_levels_val=*/3, histograms, get_type_info(), /*num_rows=*/1, /*row_stride_samples=*/1, /*is_evenly_segmented=*/true, build_info.get_cc_major(), build_info.get_cc_minor(), build_info.get_cub_path(), build_info.get_thrust_path(), build_info.get_libcudacxx_path(), build_info.get_ctk_path())); CHECK(build.cc == build_info.get_cc_major() * 10 + build_info.get_cc_minor()); CHECK((build.payload != nullptr && build.payload_kind == CCCL_PAYLOAD_CUBIN)); CHECK(build.payload_size > 0); CHECK(build.runtime_policy != nullptr); CHECK(build.runtime_policy_size > 0); REQUIRE(build.init_kernel_lowered_name != nullptr); CHECK(build.init_kernel_lowered_name[0] != '\0'); REQUIRE(build.sweep_kernel_lowered_name != nullptr); CHECK(build.sweep_kernel_lowered_name[0] != '\0'); REQUIRE(CUDA_SUCCESS == cccl_device_histogram_cleanup(&build)); } C2H_TEST("Histogram compile/load round-trip", "[histogram][device][serialization]") { using T = int32_t; constexpr int device_id = 0; const auto& build_info = BuildInformation::init(); pointer_t dummy_samples(1); pointer_t dummy_histograms(1); value_t lower_level_val{T{0}}; cccl_device_histogram_build_result_t build{}; REQUIRE( CUDA_SUCCESS == cccl_device_histogram_compile( &build, /*num_channels=*/1, /*num_active_channels=*/1, dummy_samples, /*num_output_levels_val=*/3, dummy_histograms, get_type_info(), /*num_rows=*/1, /*row_stride_samples=*/1, /*is_evenly_segmented=*/true, build_info.get_cc_major(), build_info.get_cc_minor(), build_info.get_cub_path(), build_info.get_thrust_path(), build_info.get_libcudacxx_path(), build_info.get_ctk_path(), nullptr)); REQUIRE((build.payload != nullptr && build.payload_kind == CCCL_PAYLOAD_CUBIN)); REQUIRE(build.payload_size > 0); REQUIRE(build.init_kernel_lowered_name != nullptr); REQUIRE(build.sweep_kernel_lowered_name != nullptr); CHECK(build.library == nullptr); CHECK(build.init_kernel == nullptr); REQUIRE(CUDA_SUCCESS == cccl_device_histogram_load(&build)); REQUIRE(build.library != nullptr); CHECK(build.init_kernel != nullptr); CHECK(build.sweep_kernel != nullptr); // 16 samples uniformly in [0, 4), 2 bins: [0,2) and [2,4) constexpr std::size_t n_samples = 16; const std::vector samples = {0, 0, 1, 1, 2, 2, 3, 3, 0, 1, 2, 3, 0, 1, 2, 3}; pointer_t samples_ptr(samples); pointer_t histogram_ptr(2); // 2 bins value_t upper_level_val{T{4}}; value_t num_levels_val{3}; // 3 level boundaries → 2 bins CUstream null_stream = nullptr; size_t temp_storage_bytes = 0; REQUIRE( CUDA_SUCCESS == cccl_device_histogram_even( build, nullptr, &temp_storage_bytes, samples_ptr, histogram_ptr, num_levels_val, lower_level_val, upper_level_val, /*num_row_pixels=*/static_cast(n_samples), /*num_rows=*/1, /*row_stride_samples=*/static_cast(n_samples), null_stream)); pointer_t temp_storage(temp_storage_bytes); REQUIRE( CUDA_SUCCESS == cccl_device_histogram_even( build, temp_storage.ptr, &temp_storage_bytes, samples_ptr, histogram_ptr, num_levels_val, lower_level_val, upper_level_val, /*num_row_pixels=*/static_cast(n_samples), /*num_rows=*/1, /*row_stride_samples=*/static_cast(n_samples), null_stream)); // samples {0,0,1,1,0,1,0,1} in bin 0 (8 samples) and {2,2,3,3,2,3,2,3} in bin 1 (8 samples) REQUIRE(histogram_ptr[0] == 8); REQUIRE(histogram_ptr[1] == 8); REQUIRE(CUDA_SUCCESS == cccl_device_histogram_cleanup(&build)); } #endif // CCCL_C_PARALLEL_V2