//===----------------------------------------------------------------------===// // // 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) 2024 NVIDIA CORPORATION & AFFILIATES. // //===----------------------------------------------------------------------===// #include #include // std::cerr #include #include #include // std::optional #include #include #include #include #include "algorithm_execution.h" #include "build_result_caching.h" #include "test_util.h" #include // serialize/deserialize is a v1-only feature; the header does not exist in // the v2 (HostJIT) include tree, and the tests using it are guarded the same way. #ifndef CCCL_C_PARALLEL_V2 # include #endif using BuildResultT = cccl_device_reduce_build_result_t; struct reduce_cleanup { CUresult operator()(BuildResultT* build_data) const noexcept { return cccl_device_reduce_cleanup(build_data); } }; using reduce_deleter = BuildResultDeleter; using reduce_build_cache_t = build_cache_t>; template auto& get_cache() { return fixture::get_or_create().get_value(); } struct reduce_build { static bool should_check_sass(int cc_major) { // TODO: add a check for NVRTC version; ref nvbug 5243118 return cc_major < 9; } CUresult operator()( BuildResultT* build_ptr, cccl_determinism_t determinism, cccl_iterator_t input, cccl_iterator_t output, uint64_t, cccl_op_t op, cccl_value_t init, int cc_major, int cc_minor, const char* cub_path, const char* thrust_path, const char* libcudacxx_path, const char* ctk_path) const noexcept { return cccl_device_reduce_build( build_ptr, input, output, op, init, determinism, cc_major, cc_minor, cub_path, thrust_path, libcudacxx_path, ctk_path); } }; struct reduce_build_ex { static bool should_check_sass(int cc_major) { // TODO: add a check for NVRTC version; ref nvbug 5243118 return cc_major < 9; } cccl_build_config config; reduce_build_ex(const char** extra_compile_flags, size_t num_flags, const char** extra_include_dirs, size_t num_dirs) : config(make_build_config(extra_compile_flags, num_flags, extra_include_dirs, num_dirs)) {} CUresult operator()( BuildResultT* build_ptr, cccl_determinism_t determinism, cccl_iterator_t input, cccl_iterator_t output, uint64_t, cccl_op_t op, cccl_value_t init, int cc_major, int cc_minor, const char* cub_path, const char* thrust_path, const char* libcudacxx_path, const char* ctk_path) const noexcept { return cccl_device_reduce_build_ex( build_ptr, input, output, op, init, determinism, cc_major, cc_minor, cub_path, thrust_path, libcudacxx_path, ctk_path, const_cast(&config)); } }; struct reduce_run { template CUresult operator()(cccl_device_reduce_build_result_t build, void* d_temp_storage, size_t* temp_storage_bytes, cccl_determinism_t determinism, Ts... args) const noexcept { if (determinism == CCCL_NOT_GUARANTEED) { return cccl_device_reduce_nondeterministic(build, d_temp_storage, temp_storage_bytes, args...); } else { return cccl_device_reduce(build, d_temp_storage, temp_storage_bytes, args...); } } }; template void reduce(cccl_iterator_t input, cccl_iterator_t output, uint64_t num_items, cccl_op_t op, cccl_value_t init, cccl_determinism_t determinism, std::optional& cache, const std::optional& lookup_key) { AlgorithmExecute( cache, lookup_key, determinism, input, output, num_items, op, init); } // =============== // Tests section // =============== using integral_types = c2h::type_list; struct Reduce_IntegralTypes_Fixture_Tag; C2H_TEST("Reduce works with integral types", "[reduce]", integral_types) { using T = c2h::get<0, TestType>; const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 24))); operation_t op = make_operation("op", get_reduce_op(get_type_info().type)); const std::vector input = generate(num_items); pointer_t input_ptr(input); pointer_t output_ptr(1); value_t init{T{42}}; auto& build_cache = get_cache(); const auto& test_key = make_key(); reduce(input_ptr, output_ptr, num_items, op, init, CCCL_RUN_TO_RUN, build_cache, test_key); const T output = output_ptr[0]; const T expected = std::accumulate(input.begin(), input.end(), init.value); REQUIRE(output == expected); } struct Reduce_IntegralTypes_WellKnown_Fixture_Tag; C2H_TEST("Reduce works with integral types with well-known operations", "[reduce][well_known]", integral_types) { using T = c2h::get<0, TestType>; const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 24))); cccl_op_t op = make_well_known_binary_operation(); const std::vector input = generate(num_items); pointer_t input_ptr(input); pointer_t output_ptr(1); value_t init{T{42}}; auto& build_cache = get_cache(); const auto& test_key = make_key(); reduce(input_ptr, output_ptr, num_items, op, init, CCCL_RUN_TO_RUN, build_cache, test_key); const T output = output_ptr[0]; const T expected = std::accumulate(input.begin(), input.end(), init.value); REQUIRE(output == expected); } struct pair { short a; size_t b; }; struct Reduce_CustomTypes_Fixture_Tag; C2H_TEST("Reduce works with custom types", "[reduce]") { const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 24))); operation_t op = make_operation("op", R"(struct pair { short a; size_t b; }; extern "C" __device__ void op(void* lhs_ptr, void* rhs_ptr, void* out_ptr) { pair* lhs = static_cast(lhs_ptr); pair* rhs = static_cast(rhs_ptr); pair* out = static_cast(out_ptr); *out = pair{ lhs->a + rhs->a, lhs->b + rhs->b }; })"); const std::vector a = generate(num_items); const std::vector b = generate(num_items); std::vector input(num_items); for (std::size_t i = 0; i < num_items; ++i) { input[i] = pair{a[i], b[i]}; } pointer_t input_ptr(input); pointer_t output_ptr(1); value_t init{pair{4, 2}}; auto& build_cache = get_cache(); const auto& test_key = make_key(); reduce(input_ptr, output_ptr, num_items, op, init, CCCL_RUN_TO_RUN, build_cache, test_key); const pair output = output_ptr[0]; const pair expected = std::accumulate(input.begin(), input.end(), init.value, [](const pair& lhs, const pair& rhs) { return pair{short(lhs.a + rhs.a), lhs.b + rhs.b}; }); REQUIRE(output.a == expected.a); REQUIRE(output.b == expected.b); } struct Reduce_CustomTypes_WellKnown_Fixture_Tag; C2H_TEST("Reduce works with custom types with well-known operations", "[reduce][well_known]") { const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 24))); operation_t op_state = make_operation("op", R"(struct pair { short a; size_t b; }; extern "C" __device__ void op(void* lhs_ptr, void* rhs_ptr, void* out_ptr) { pair* lhs = static_cast(lhs_ptr); pair* rhs = static_cast(rhs_ptr); pair* out = static_cast(out_ptr); *out = pair{ lhs->a + rhs->a, lhs->b + rhs->b }; })"); cccl_op_t op = op_state; op.type = cccl_op_kind_t::CCCL_PLUS; const std::vector a = generate(num_items); const std::vector b = generate(num_items); std::vector input(num_items); for (std::size_t i = 0; i < num_items; ++i) { input[i] = pair{a[i], b[i]}; } pointer_t input_ptr(input); pointer_t output_ptr(1); value_t init{pair{4, 2}}; auto& build_cache = get_cache(); const auto& test_key = make_key(); reduce(input_ptr, output_ptr, num_items, op, init, CCCL_RUN_TO_RUN, build_cache, test_key); const pair output = output_ptr[0]; const pair expected = std::accumulate(input.begin(), input.end(), init.value, [](const pair& lhs, const pair& rhs) { return pair{short(lhs.a + rhs.a), lhs.b + rhs.b}; }); REQUIRE(output.a == expected.a); REQUIRE(output.b == expected.b); } struct Reduce_InputIterators_Fixture_Tag; C2H_TEST("Reduce works with input iterators", "[reduce]") { const std::size_t num_items = GENERATE(1, 42, take(4, random(1 << 12, 1 << 16))); operation_t op = make_operation("op", get_reduce_op(get_type_info().type)); iterator_t> input_it = make_counting_iterator("int"); input_it.state.value = 0; pointer_t output_it(1); value_t init{42}; auto& build_cache = get_cache(); const auto& test_key = make_key(); reduce(input_it, output_it, num_items, op, init, CCCL_RUN_TO_RUN, build_cache, test_key); const int output = output_it[0]; const int expected = init.value + static_cast(num_items * (num_items - 1) / 2); REQUIRE(output == expected); } struct Reduce_OutputIterators_Fixture_Tag; C2H_TEST("Reduce works with output iterators", "[reduce]") { const int num_items = GENERATE(1, 42, take(4, random(1 << 12, 1 << 16))); operation_t op = make_operation("op", get_reduce_op(get_type_info().type)); iterator_t> output_it = make_random_access_iterator(iterator_kind::OUTPUT, "int", "out", " * 2"); const std::vector input = generate(num_items); pointer_t input_it(input); pointer_t inner_output_it(1); output_it.state.data = inner_output_it.ptr; value_t init{42}; auto& build_cache = get_cache(); const auto& test_key = make_key(); reduce(input_it, output_it, num_items, op, init, CCCL_RUN_TO_RUN, build_cache, test_key); const int output = inner_output_it[0]; const int expected = std::accumulate(input.begin(), input.end(), init.value); REQUIRE(output == expected * 2); } struct Reduce_InputOutputIterators_Fixture_Tag; C2H_TEST("Reduce works with input and output iterators", "[reduce]") { const int num_items = GENERATE(1, 42, take(4, random(1 << 12, 1 << 16))); operation_t op = make_operation("op", get_reduce_op(get_type_info().type)); iterator_t> input_it = make_constant_iterator("int"); input_it.state.value = 1; iterator_t> output_it = make_random_access_iterator(iterator_kind::OUTPUT, "int", "out", " * 2"); pointer_t inner_output_it(1); output_it.state.data = inner_output_it.ptr; value_t init{42}; auto& build_cache = get_cache(); const auto& test_key = make_key(); reduce(input_it, output_it, num_items, op, init, CCCL_RUN_TO_RUN, build_cache, test_key); const int output = inner_output_it[0]; const int expected = 2 * (init.value + num_items); REQUIRE(output == expected); } struct Reduce_AccumulatorType_Fixture_Tag; C2H_TEST("Reduce accumulator type is influenced by initial value", "[reduce]") { const std::size_t num_items = 1 << 14; // 16384 > 128 operation_t op = make_operation("op", get_reduce_op(get_type_info().type)); iterator_t> input_it = make_constant_iterator("char"); input_it.state.value = 1; pointer_t output_it(1); value_t init{42}; auto& build_cache = get_cache(); const auto& test_key = make_key(); reduce(input_it, output_it, num_items, op, init, CCCL_RUN_TO_RUN, build_cache, test_key); const size_t output = output_it[0]; const size_t expected = init.value + num_items; REQUIRE(output == expected); } C2H_TEST("Reduce works with large inputs", "[reduce]") { const size_t num_items = 1ull << 33; operation_t op = make_operation("op", get_reduce_op(get_type_info().type)); iterator_t> input_it = make_constant_iterator("char"); input_it.state.value = 1; pointer_t output_it(1); value_t init{42}; // reuse fixture cache from previous example, as it runs identical example on larger input auto& build_cache = get_cache(); const auto& test_key = make_key(); reduce(input_it, output_it, num_items, op, init, CCCL_RUN_TO_RUN, build_cache, test_key); const size_t output = output_it[0]; const size_t expected = init.value + num_items; REQUIRE(output == expected); } struct invocation_counter_state_t { int* d_counter; }; C2H_TEST("Reduce works with stateful operators", "[reduce]") { const int num_items = 1 << 12; pointer_t counter(1); stateful_operation_t op = make_operation( "op", R"(struct invocation_counter_state_t { int* d_counter; }; extern "C" __device__ void op(void* state_ptr, void* a_ptr, void* b_ptr, void* out_ptr) { invocation_counter_state_t* state = static_cast(state_ptr); atomicAdd(state->d_counter, 1); int a = *static_cast(a_ptr); int b = *static_cast(b_ptr); *static_cast(out_ptr) = a + b; })", invocation_counter_state_t{counter.ptr}); const std::vector input = generate(num_items); pointer_t input_ptr(input); pointer_t output_ptr(1); value_t init{42}; // turn off caching, since the example is only compiled once std::optional build_cache = std::nullopt; std::optional test_key = std::nullopt; reduce(input_ptr, output_ptr, num_items, op, init, CCCL_RUN_TO_RUN, build_cache, test_key); const int invocation_count = counter[0]; const int expected_invocation_count = num_items - 1; REQUIRE(invocation_count > expected_invocation_count); const int output = output_ptr[0]; const int expected = std::accumulate(input.begin(), input.end(), init.value); REQUIRE(output == expected); } C2H_TEST("Reduce works with C++ source operations", "[reduce]") { using T = int32_t; const std::size_t num_items = GENERATE(42, 1337, 42000); // Create operation from C++ source instead of LTO-IR std::string cpp_source = R"( extern "C" __device__ void op(void* a, void* b, void* out) { int* ia = (int*)a; int* ib = (int*)b; int* iout = (int*)out; *iout = *ia + *ib; } )"; operation_t op = make_cpp_operation("op", cpp_source); const std::vector input = generate(num_items); pointer_t input_ptr(input); pointer_t output_ptr(1); value_t init{T{0}}; // Test key including flag that this uses C++ source std::optional test_key = std::format("cpp_source_test_{}_{}", num_items, typeid(T).name()); auto& cache = get_cache(); std::optional cache_opt = cache; reduce(input_ptr, output_ptr, num_items, op, init, CCCL_RUN_TO_RUN, cache_opt, test_key); const T output = output_ptr[0]; const T expected = std::accumulate(input.begin(), input.end(), init.value); REQUIRE(output == expected); } struct Reduce_FloatingPointTypes_Fixture_Tag; using floating_point_types = c2h::type_list< #if _CCCL_HAS_NVFP16() __half, #endif float, double>; C2H_TEST("Reduce works with floating point types", "[reduce]", floating_point_types) { using T = c2h::get<0, TestType>; // Use small input sizes and values to avoid floating point precision issues. const std::size_t num_items = GENERATE(10, 42, 1025); operation_t op = make_operation("op", get_reduce_op(get_type_info().type)); const std::vector input(num_items, T{1}); pointer_t input_ptr(input); pointer_t output_ptr(1); value_t init{T{42}}; auto& build_cache = get_cache(); const auto& test_key = make_key(); reduce(input_ptr, output_ptr, num_items, op, init, CCCL_RUN_TO_RUN, build_cache, test_key); const T output = output_ptr[0]; const T expected = std::accumulate(input.begin(), input.end(), init.value); REQUIRE_APPROX_EQ(std::vector{output}, std::vector{expected}); } struct Reduce_CppSourceWithEx_Fixture_Tag; C2H_TEST("Reduce works with C++ source operations using _ex build", "[reduce]") { using T = int32_t; const std::size_t num_items = GENERATE(42, 1337, 42000); // Create operation from C++ source that uses the identity function from header std::string cpp_source = R"( #include "test_identity.h" extern "C" __device__ void op(void* a, void* b, void* out) { int* ia = (int*)a; int* ib = (int*)b; int* iout = (int*)out; int val_a = test_identity(*ia); int val_b = test_identity(*ib); *iout = val_a + val_b; } )"; operation_t op = make_cpp_operation("op", cpp_source); const std::vector input = generate(num_items); pointer_t input_ptr(input); pointer_t output_ptr(1); value_t init{T{0}}; // Prepare extra compile flags and include paths const char* extra_flags[] = {"-DTEST_IDENTITY_ENABLED"}; const char* extra_includes[] = {TEST_INCLUDE_PATH}; // Use extended AlgorithmExecute with custom build configuration constexpr int device_id = 0; const auto& build_info = BuildInformation::init(); BuildResultT build{}; reduce_build_ex builder(extra_flags, 1, extra_includes, 1); REQUIRE( CUDA_SUCCESS == builder( &build, CCCL_RUN_TO_RUN, input_ptr, output_ptr, num_items, op, init, 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())); CUstream null_stream = nullptr; size_t temp_storage_bytes = 0; REQUIRE(CUDA_SUCCESS == cccl_device_reduce( build, nullptr, &temp_storage_bytes, input_ptr, output_ptr, num_items, op, init, null_stream)); pointer_t temp_storage(temp_storage_bytes); REQUIRE(CUDA_SUCCESS == cccl_device_reduce( build, temp_storage.ptr, &temp_storage_bytes, input_ptr, output_ptr, num_items, op, init, null_stream)); const T output = output_ptr[0]; const T expected = std::accumulate(input.begin(), input.end(), init.value); REQUIRE(output == expected); // Cleanup REQUIRE(CUDA_SUCCESS == cccl_device_reduce_cleanup(&build)); } #ifndef CCCL_C_PARALLEL_V2 C2H_TEST("Reduce build result has serialization metadata populated", "[reduce][serialization]") { using T = int32_t; constexpr int device_id = 0; const auto& build_info = BuildInformation::init(); operation_t op = make_operation("op", get_reduce_op(get_type_info().type)); const std::vector input = generate(16); pointer_t input_ptr(input); pointer_t output_ptr(1); value_t init{T{0}}; BuildResultT build{}; REQUIRE( CUDA_SUCCESS == cccl_device_reduce_build( &build, input_ptr, output_ptr, op, init, CCCL_RUN_TO_RUN, 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())); // cc field is packed as cc_major * 10 + cc_minor 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); REQUIRE(build.single_tile_kernel_lowered_name != nullptr); CHECK(build.single_tile_kernel_lowered_name[0] != '\0'); REQUIRE(build.single_tile_second_kernel_lowered_name != nullptr); CHECK(build.single_tile_second_kernel_lowered_name[0] != '\0'); REQUIRE(build.reduction_kernel_lowered_name != nullptr); CHECK(build.reduction_kernel_lowered_name[0] != '\0'); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_cleanup(&build)); } struct Reduce_Nondeterministic_Plus_Fixture_Tag; C2H_TEST("Reduce works with not_guaranteed determinism and plus", "[reduce][nondeterministic]") { using T = float; const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 24))); cccl_op_t op = make_well_known_binary_operation(); // plus const std::vector input(num_items, T{1}); pointer_t input_ptr(input); pointer_t output_ptr(1); value_t init{T{0}}; auto& build_cache = get_cache(); const auto& test_key = make_key(); reduce(input_ptr, output_ptr, num_items, op, init, CCCL_NOT_GUARANTEED, build_cache, test_key); const T output = output_ptr[0]; const T expected = std::accumulate(input.begin(), input.end(), init.value); REQUIRE(output == expected); } C2H_TEST("Reduce compile/load round-trip", "[reduce][serialization]") { using T = int32_t; constexpr int device_id = 0; const auto& build_info = BuildInformation::init(); cccl_op_t op = make_well_known_binary_operation(); // plus pointer_t dummy_in(1); pointer_t dummy_out(1); value_t init{T{0}}; BuildResultT build{}; REQUIRE( CUDA_SUCCESS == cccl_device_reduce_compile( &build, dummy_in, dummy_out, op, init, CCCL_RUN_TO_RUN, 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.single_tile_kernel_lowered_name != nullptr); REQUIRE(build.single_tile_second_kernel_lowered_name != nullptr); REQUIRE(build.reduction_kernel_lowered_name != nullptr); CHECK(build.library == nullptr); CHECK(build.single_tile_kernel == nullptr); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_load(&build)); REQUIRE(build.library != nullptr); CHECK(build.single_tile_kernel != nullptr); CHECK(build.single_tile_second_kernel != nullptr); CHECK(build.reduction_kernel != nullptr); constexpr std::size_t n = 16; const std::vector input = generate(n); pointer_t input_ptr(input); pointer_t output_ptr(1); CUstream null_stream = nullptr; size_t temp_storage_bytes = 0; REQUIRE(CUDA_SUCCESS == cccl_device_reduce(build, nullptr, &temp_storage_bytes, input_ptr, output_ptr, n, op, init, null_stream)); pointer_t temp_storage(temp_storage_bytes); REQUIRE(CUDA_SUCCESS == cccl_device_reduce( build, temp_storage.ptr, &temp_storage_bytes, input_ptr, output_ptr, n, op, init, null_stream)); const T result = output_ptr[0]; const T expected = std::accumulate(input.begin(), input.end(), T{0}); REQUIRE(result == expected); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_cleanup(&build)); } C2H_TEST("Reduce link_ltoir round-trip", "[reduce][serialization]") { using T = int32_t; constexpr int device_id = 0; const auto& build_info = BuildInformation::init(); // Kernel-only compile: op has a name but no LTOIR (code_size == 0). // compile() will produce kernel LTOIR with an unresolved external reference to "op". cccl_op_t op_ko{}; op_ko.type = CCCL_STATELESS; op_ko.name = "op"; op_ko.code = nullptr; op_ko.code_size = 0; op_ko.code_type = CCCL_OP_LTOIR; pointer_t dummy_in(1); pointer_t dummy_out(1); value_t init{T{0}}; BuildResultT build{}; REQUIRE( CUDA_SUCCESS == cccl_device_reduce_compile( &build, dummy_in, dummy_out, op_ko, init, CCCL_RUN_TO_RUN, 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)); // After kernel-only compile: kernel_ltoir is populated, cubin is not. REQUIRE((build.payload != nullptr && build.payload_kind == CCCL_PAYLOAD_LTOIR)); REQUIRE(build.payload_size > 0); CHECK((build.payload_kind != CCCL_PAYLOAD_CUBIN)); CHECK(build.library == nullptr); // Compile the operator LTOIR separately (this is the "user-supplied" op blob). operation_t op_full = make_operation("op", get_reduce_op(get_type_info().type)); const void* op_blob = op_full.code.data(); size_t op_size = op_full.code.size(); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_link_ltoir(&build, &op_blob, &op_size, 1)); REQUIRE((build.payload != nullptr && build.payload_kind == CCCL_PAYLOAD_CUBIN)); REQUIRE(build.library == nullptr); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_load(&build)); REQUIRE(build.library != nullptr); CHECK((build.payload_kind != CCCL_PAYLOAD_LTOIR)); constexpr std::size_t n = 16; const std::vector input = generate(n); pointer_t input_ptr(input); pointer_t output_ptr(1); CUstream null_stream = nullptr; size_t temp_storage_bytes = 0; cccl_op_t op_run = op_full; cccl_value_t init_v = init; REQUIRE( CUDA_SUCCESS == cccl_device_reduce(build, nullptr, &temp_storage_bytes, input_ptr, output_ptr, n, op_run, init_v, null_stream)); pointer_t temp_storage(temp_storage_bytes); REQUIRE(CUDA_SUCCESS == cccl_device_reduce( build, temp_storage.ptr, &temp_storage_bytes, input_ptr, output_ptr, n, op_run, init_v, null_stream)); const T result = output_ptr[0]; const T expected = std::accumulate(input.begin(), input.end(), T{0}); REQUIRE(result == expected); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_cleanup(&build)); } C2H_TEST("Reduce serialize/deserialize round-trip (cubin)", "[reduce][serialization]") { using T = int32_t; constexpr int device_id = 0; const auto& build_info = BuildInformation::init(); cccl_op_t op = make_well_known_binary_operation(); // plus pointer_t dummy_in(1); pointer_t dummy_out(1); value_t init{T{0}}; // Compile (full op → cubin payload) BuildResultT build_a{}; REQUIRE( CUDA_SUCCESS == cccl_device_reduce_compile( &build_a, dummy_in, dummy_out, op, init, CCCL_RUN_TO_RUN, 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_a.payload != nullptr && build_a.payload_kind == CCCL_PAYLOAD_CUBIN)); // Serialize void* blob = nullptr; size_t blob_size = 0; REQUIRE(CUDA_SUCCESS == cccl_device_reduce_serialize(&build_a, &blob, &blob_size)); REQUIRE(blob != nullptr); REQUIRE(blob_size > 0); // Cleanup the original — proves the serialized blob is self-contained. REQUIRE(CUDA_SUCCESS == cccl_device_reduce_cleanup(&build_a)); // Deserialize into a fresh build and load. BuildResultT build_b{}; REQUIRE(CUDA_SUCCESS == cccl_device_reduce_deserialize(&build_b, blob, blob_size)); REQUIRE(build_b.payload != nullptr); REQUIRE(build_b.payload_kind == CCCL_PAYLOAD_CUBIN); REQUIRE(build_b.runtime_policy != nullptr); REQUIRE(build_b.single_tile_kernel_lowered_name != nullptr); CHECK(build_b.library == nullptr); CHECK(build_b.single_tile_kernel == nullptr); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_load(&build_b)); REQUIRE(build_b.library != nullptr); constexpr std::size_t n = 16; const std::vector input = generate(n); pointer_t input_ptr(input); pointer_t output_ptr(1); CUstream null_stream = nullptr; size_t temp_storage_bytes = 0; REQUIRE( CUDA_SUCCESS == cccl_device_reduce(build_b, nullptr, &temp_storage_bytes, input_ptr, output_ptr, n, op, init, null_stream)); pointer_t temp_storage(temp_storage_bytes); REQUIRE(CUDA_SUCCESS == cccl_device_reduce( build_b, temp_storage.ptr, &temp_storage_bytes, input_ptr, output_ptr, n, op, init, null_stream)); const T result = output_ptr[0]; const T expected = std::accumulate(input.begin(), input.end(), T{0}); REQUIRE(result == expected); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_cleanup(&build_b)); cccl_serialization_buffer_free(blob); } C2H_TEST("Reduce serialize/deserialize round-trip (ltoir + link_ltoir)", "[reduce][serialization]") { using T = int32_t; constexpr int device_id = 0; const auto& build_info = BuildInformation::init(); // Kernel-only compile produces an LTOIR payload. cccl_op_t op_ko{}; op_ko.type = CCCL_STATELESS; op_ko.name = "op"; op_ko.code = nullptr; op_ko.code_size = 0; op_ko.code_type = CCCL_OP_LTOIR; pointer_t dummy_in(1); pointer_t dummy_out(1); value_t init{T{0}}; BuildResultT build_a{}; REQUIRE( CUDA_SUCCESS == cccl_device_reduce_compile( &build_a, dummy_in, dummy_out, op_ko, init, CCCL_RUN_TO_RUN, 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_a.payload != nullptr && build_a.payload_kind == CCCL_PAYLOAD_LTOIR)); void* blob = nullptr; size_t blob_size = 0; REQUIRE(CUDA_SUCCESS == cccl_device_reduce_serialize(&build_a, &blob, &blob_size)); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_cleanup(&build_a)); BuildResultT build_b{}; REQUIRE(CUDA_SUCCESS == cccl_device_reduce_deserialize(&build_b, blob, blob_size)); REQUIRE(build_b.payload_kind == CCCL_PAYLOAD_LTOIR); // Link in the operator LTOIR (as user-supplied) and load. operation_t op_full = make_operation("op", get_reduce_op(get_type_info().type)); const void* op_blob = op_full.code.data(); size_t op_size = op_full.code.size(); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_link_ltoir(&build_b, &op_blob, &op_size, 1)); REQUIRE(build_b.payload_kind == CCCL_PAYLOAD_CUBIN); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_load(&build_b)); constexpr std::size_t n = 16; const std::vector input = generate(n); pointer_t input_ptr(input); pointer_t output_ptr(1); CUstream null_stream = nullptr; size_t temp_storage_bytes = 0; cccl_op_t op_run = op_full; REQUIRE( CUDA_SUCCESS == cccl_device_reduce(build_b, nullptr, &temp_storage_bytes, input_ptr, output_ptr, n, op_run, init, null_stream)); pointer_t temp_storage(temp_storage_bytes); REQUIRE(CUDA_SUCCESS == cccl_device_reduce( build_b, temp_storage.ptr, &temp_storage_bytes, input_ptr, output_ptr, n, op_run, init, null_stream)); REQUIRE(output_ptr[0] == std::accumulate(input.begin(), input.end(), T{0})); REQUIRE(CUDA_SUCCESS == cccl_device_reduce_cleanup(&build_b)); cccl_serialization_buffer_free(blob); } C2H_TEST("Reduce deserialize rejects bad blobs", "[reduce][serialization]") { BuildResultT build{}; // Empty input. REQUIRE(CUDA_ERROR_INVALID_VALUE == cccl_device_reduce_deserialize(&build, nullptr, 0)); // Garbage bytes that are large enough to read a header from. const char garbage[64] = {0}; REQUIRE(CUDA_SUCCESS != cccl_device_reduce_deserialize(&build, garbage, sizeof(garbage))); // On failure build is zeroed. CHECK(build.payload == nullptr); } #endif // CCCL_C_PARALLEL_V2