#include #include #include #include // std::optional #include #include #include "algorithm_execution.h" #include "build_result_caching.h" #include "test_util.h" #include #include using BuildResultT = cccl_device_transform_build_result_t; struct transform_cleanup { CUresult operator()(BuildResultT* build_data) const noexcept { return cccl_device_transform_cleanup(build_data); } }; using transform_deleter = BuildResultDeleter; using transform_build_cache_t = build_cache_t>; template auto& get_cache() { return fixture::get_or_create().get_value(); } struct transform_build { using IterT = cccl_iterator_t; template CUresult operator()(BuildResultT* build_ptr, IterT input, IterT output, uint64_t, Ts... rest) const noexcept { return cccl_device_unary_transform_build(build_ptr, input, output, rest...); } template CUresult operator()(BuildResultT* build_ptr, IterT input1, IterT input2, IterT output, uint64_t, Ts... rest) const noexcept { return cccl_device_binary_transform_build(build_ptr, input1, input2, output, rest...); } }; struct unary_transform_run { template CUresult operator()(BuildResultT build, void* scratch, size_t* scratch_size, Ts... args) const noexcept { *scratch_size = 1; return (scratch) ? cccl_device_unary_transform(build, args...) : CUDA_SUCCESS; } }; struct binary_transform_run { template CUresult operator()(BuildResultT build, void* scratch, size_t* scratch_size, Ts... args) const noexcept { *scratch_size = 1; return (scratch) ? cccl_device_binary_transform(build, args...) : CUDA_SUCCESS; } }; template void unary_transform( cccl_iterator_t input, cccl_iterator_t output, uint64_t num_items, cccl_op_t op, std::optional& cache, const std::optional& lookup_key) { AlgorithmExecute( cache, lookup_key, input, output, num_items, op); } template void binary_transform( cccl_iterator_t input1, cccl_iterator_t input2, cccl_iterator_t output, uint64_t num_items, cccl_op_t op, std::optional& cache, const std::optional& lookup_key) { AlgorithmExecute( cache, lookup_key, input1, input2, output, num_items, op); } C2H_TEST("Transform generates UBLKCP on SM90", "[transform][ublkcp]") { constexpr int device_id = 0; const auto& build_info = BuildInformation::init(); // Only test for ublkcp when it is actually possible to get it. if (build_info.get_cc_major() < 9) { return; } cccl_device_transform_build_result_t build{}; operation_t op = make_operation("op", get_unary_op(get_type_info().type)); REQUIRE( CUDA_SUCCESS == cccl_device_unary_transform_build( &build, pointer_t(0), pointer_t(0), op, 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())); std::string sass = inspect_sass(build.payload, build.payload_size); CHECK(sass.find("UBLKCP") != std::string::npos); op = make_operation("op", get_reduce_op(get_type_info().type)); REQUIRE( CUDA_SUCCESS == cccl_device_binary_transform_build( &build, pointer_t(0), pointer_t(0), pointer_t(0), op, 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())); sass = inspect_sass(build.payload, build.payload_size); CHECK(sass.find("UBLKCP") != std::string::npos); } using integral_types = c2h::type_list; struct Transform_IntegralTypes_Fixture_Tag; C2H_TEST("Transform works with integral types", "[transform]", integral_types) { using T = c2h::get<0, TestType>; const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 16))); operation_t op = make_operation("op", get_unary_op(get_type_info().type)); const std::vector input = generate(num_items); const std::vector output(num_items, 0); pointer_t input_ptr(input); pointer_t output_ptr(output); auto& build_cache = get_cache(); const auto& test_key = make_key(); unary_transform(input_ptr, output_ptr, num_items, op, build_cache, test_key); std::vector expected(num_items, 0); std::transform(input.begin(), input.end(), expected.begin(), [](const T& x) { return 2 * x; }); if (num_items > 0) { REQUIRE(expected == std::vector(output_ptr)); } } struct Transform_MisalignedInput_IntegerTypes_Fixture_Tag; C2H_TEST("Transform works with misaligned input with integral types", "[transform]", integral_types) { using T = c2h::get<0, TestType>; const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 16))); operation_t op = make_operation("op", get_unary_op(get_type_info().type)); const std::vector input = generate(num_items + 1); const std::vector output(num_items, 0); pointer_t input_ptr_aligned(input); pointer_t input_ptr = input; input_ptr.ptr += 1; // misalign by 1 from the guaranteed alignment of cudaMalloc, to maybe trip vectorized path input_ptr.size -= 1; pointer_t output_ptr(output); auto& build_cache = get_cache(); const auto& test_key = make_key(); unary_transform(input_ptr, output_ptr, num_items, op, build_cache, test_key); input_ptr.ptr = nullptr; // avoid freeing the memory through this pointer std::vector expected(num_items, 0); std::transform(input.begin() + 1, input.end(), expected.begin(), [](const T& x) { return 2 * x; }); REQUIRE(expected == std::vector(output_ptr)); } struct Transform_MisalignedOutput_IntegerTypes_Fixture_Tag; C2H_TEST("Transform works with misaligned output with integral types", "[transform]", integral_types) { using T = c2h::get<0, TestType>; const std::size_t num_items = GENERATE(1, 42, take(4, random(1 << 12, 1 << 16))); operation_t op = make_operation("op", get_unary_op(get_type_info().type)); const std::vector input = generate(num_items); const std::vector output(num_items + 1, 0); pointer_t input_ptr(input); pointer_t output_ptr_aligned(output); pointer_t output_ptr = output; output_ptr.ptr += 1; // misalign by 1 from the guaranteed alignment of cudaMalloc, to maybe trip vectorized path output_ptr.size -= 1; auto& build_cache = get_cache(); const auto& test_key = make_key(); unary_transform(input_ptr, output_ptr, num_items, op, build_cache, test_key); std::vector expected(num_items, 0); std::transform(input.begin(), input.end(), expected.begin(), [](const T& x) { return 2 * x; }); REQUIRE(expected == std::vector(output_ptr)); output_ptr.ptr = nullptr; // avoid freeing the memory through this pointer } struct Transform_IntegralTypes_WellKnown_Fixture_Tag; C2H_TEST("Transform works with integral types with well-known operations", "[transform][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 << 16))); cccl_op_t op = make_well_known_unary_operation(); const std::vector input = generate(num_items); const std::vector output(num_items, 0); pointer_t input_ptr(input); pointer_t output_ptr(output); auto& build_cache = get_cache(); const auto& test_key = make_key(); unary_transform(input_ptr, output_ptr, num_items, op, build_cache, test_key); std::vector expected(num_items, 0); _CCCL_DIAG_PUSH _CCCL_DIAG_SUPPRESS_MSVC(4146) // unary minus on unsigned type std::transform(input.begin(), input.end(), expected.begin(), [](const T& x) { return -x; }); _CCCL_DIAG_POP if (num_items > 0) { REQUIRE(expected == std::vector(output_ptr)); } } C2H_TEST("Transform works with logical and bitwise well-known operations", "[transform][well_known]") { std::optional no_cache = std::nullopt; const std::optional no_key = std::nullopt; { const std::vector input{0U, 1U, 0xaaaaaaaaU, 0xffffffffU}; pointer_t input_ptr(input); pointer_t output_ptr(input.size()); cccl_op_t bit_not_op = make_well_known_unary_operation(); bit_not_op.type = cccl_op_kind_t::CCCL_BIT_NOT; unary_transform(input_ptr, output_ptr, input.size(), bit_not_op, no_cache, no_key); REQUIRE(std::vector(output_ptr) == std::vector{0xffffffffU, 0xfffffffeU, 0x55555555U, 0U}); } const std::vector lhs{1, 1, 0, 0}; const std::vector rhs{1, 0, 1, 0}; pointer_t lhs_ptr(lhs); pointer_t rhs_ptr(rhs); const auto check_logical_op = [&](cccl_op_kind_t kind, const std::vector& expected) { pointer_t output_ptr(lhs.size()); cccl_op_t op = make_well_known_binary_operation(); op.type = kind; binary_transform( make_boolean_iterator(lhs_ptr), make_boolean_iterator(rhs_ptr), make_boolean_iterator(output_ptr), lhs.size(), op, no_cache, no_key); REQUIRE(std::vector(output_ptr) == expected); }; check_logical_op(cccl_op_kind_t::CCCL_LOGICAL_AND, {1, 0, 0, 0}); check_logical_op(cccl_op_kind_t::CCCL_LOGICAL_OR, {1, 1, 1, 0}); } struct pair { short a; size_t b; bool operator==(const pair& other) const { return a == other.a && b == other.b; } }; struct custom_int { int value; }; C2H_TEST("Transform works with C++ source for custom types with a well-known unary operation", "[transform][well_known][cpp_source]") { const std::string source = R"( struct custom_int { int value; }; extern "C" __device__ void logical_not_custom_int(void* input_ptr, void* output_ptr) { const custom_int* input = static_cast(input_ptr); bool* output = static_cast(output_ptr); *output = !input->value; } )"; const std::vector input{{0}, {1}, {-2}, {42}}; pointer_t input_ptr(input); std::optional no_cache = std::nullopt; const std::optional no_key = std::nullopt; operation_t op_state = make_cpp_operation("logical_not_custom_int", source); cccl_op_t op = op_state; op.type = cccl_op_kind_t::CCCL_LOGICAL_NOT; pointer_t output_ptr(input.size()); unary_transform(input_ptr, make_boolean_iterator(output_ptr), input.size(), op, no_cache, no_key); REQUIRE(std::vector(output_ptr) == std::vector{1, 0, 0, 0}); } struct Transform_DifferentOutputTypes_Fixture_Tag; C2H_TEST("Transform works with output of different type", "[transform]") { 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* x_ptr, void* out_ptr) { int* x = static_cast(x_ptr); pair* out = static_cast(out_ptr); *out = pair{ short(*x), size_t(*x) }; })"); const std::vector input = generate(num_items); std::vector expected(num_items); std::vector output(num_items); for (std::size_t i = 0; i < num_items; ++i) { expected[i] = {short(input[i]), size_t(input[i])}; } pointer_t input_ptr(input); pointer_t output_ptr(output); auto& build_cache = get_cache(); const auto& test_key = make_key(); unary_transform(input_ptr, output_ptr, num_items, op, build_cache, test_key); if (num_items > 0) { REQUIRE(expected == std::vector(output_ptr)); } } struct alignas(8) unary_storage_in { int x; short y; }; struct alignas(16) unary_storage_out { long long sum; int diff; bool operator==(const unary_storage_out& other) const { return sum == other.sum && diff == other.diff; } }; struct Transform_UnaryStorageTypes_Fixture_Tag; C2H_TEST("Transform works with unary storage types of different size/alignment", "[transform]") { const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 16))); operation_t op = make_operation("op", R"(struct alignas(8) unary_storage_in { int x; short y; }; struct alignas(16) unary_storage_out { long long sum; int diff; }; extern "C" __device__ void op(void* x_ptr, void* out_ptr) { auto* x = static_cast(x_ptr); auto* out = static_cast(out_ptr); out->sum = static_cast(x->x) + x->y; out->diff = x->x - x->y; })"); std::vector input(num_items); std::vector output(num_items); std::vector expected(num_items); for (std::size_t i = 0; i < num_items; ++i) { input[i] = {static_cast(i + 3), static_cast(i % 7)}; expected[i] = {static_cast(input[i].x) + input[i].y, input[i].x - input[i].y}; } pointer_t input_ptr(input); pointer_t output_ptr(output); auto& build_cache = get_cache(); const auto& test_key = make_key(); unary_transform(input_ptr, output_ptr, num_items, op, build_cache, test_key); if (num_items > 0) { REQUIRE(expected == std::vector(output_ptr)); } } struct Transform_CustomTypes_Fixture_Tag; C2H_TEST("Transform works with custom types", "[transform]") { 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* x_ptr, void* out_ptr) { pair* x = static_cast(x_ptr); pair* out = static_cast(out_ptr); *out = pair{ x->a * 2, x->b * 2 }; })"); const std::vector a = generate(num_items); const std::vector b = generate(num_items); std::vector input(num_items); std::vector output(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(output); auto& build_cache = get_cache(); const auto& test_key = make_key(); unary_transform(input_ptr, output_ptr, num_items, op, build_cache, test_key); std::vector expected(num_items, {0, 0}); std::transform(input.begin(), input.end(), expected.begin(), [](const pair& x) { return pair{short(x.a * 2), x.b * 2}; }); if (num_items > 0) { REQUIRE(expected == std::vector(output_ptr)); } } struct Transform_CustomTypes_WellKnown_Fixture_Tag; C2H_TEST("Transform works with custom types with well-known operators", "[transform][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* x_ptr, void* out_ptr) { pair* x = static_cast(x_ptr); pair* out = static_cast(out_ptr); *out = pair{ x->a * 2, x->b * 2 }; })"); cccl_op_t op = op_state; // HACK: this doesn't actually match the operation above, but that's fine, as we are supposed to not take the // well-known path anyway op.type = cccl_op_kind_t::CCCL_NEGATE; const std::vector a = generate(num_items); const std::vector b = generate(num_items); std::vector input(num_items); std::vector output(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(output); auto& build_cache = get_cache(); const auto& test_key = make_key(); unary_transform(input_ptr, output_ptr, num_items, op, build_cache, test_key); std::vector expected(num_items, {0, 0}); std::transform(input.begin(), input.end(), expected.begin(), [](const pair& x) { return pair{short(x.a * 2), x.b * 2}; }); if (num_items > 0) { REQUIRE(expected == std::vector(output_ptr)); } } struct Transform_InputIterators_Fixture_Tag; C2H_TEST("Transform works with input iterators", "[transform]") { const std::size_t num_items = GENERATE(1, 42, take(1, random(1 << 12, 1 << 16))); operation_t op = make_operation("op", get_unary_op(get_type_info().type)); iterator_t> input_it = make_counting_iterator("int"); input_it.state.value = 0; pointer_t output_it(num_items); auto& build_cache = get_cache(); const auto& test_key = make_key(); unary_transform(input_it, output_it, num_items, op, build_cache, test_key); // vector storing a sequence of values 0, 1, 2, ..., num_items - 1 std::vector input(num_items); std::iota(input.begin(), input.end(), 0); std::vector expected(num_items); std::transform(input.begin(), input.end(), expected.begin(), [](const int& x) { return x * 2; }); if (num_items > 0) { REQUIRE(expected == std::vector(output_it)); } } struct Transform_OutputIterators_Fixture_Tag; C2H_TEST("Transform works with output iterators", "[transform]") { const int num_items = GENERATE(1, 42, take(1, random(1 << 12, 1 << 16))); operation_t op = make_operation("op", get_unary_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(num_items); output_it.state.data = inner_output_it.ptr; auto& build_cache = get_cache(); const auto& test_key = make_key(); unary_transform(input_it, output_it, num_items, op, build_cache, test_key); std::vector expected(num_items); std::transform(input.begin(), input.end(), expected.begin(), [](int x) { return x * 4; }); if (num_items > 0) { REQUIRE(expected == std::vector(inner_output_it)); } } struct Transform_BinaryOp_Fixture_Tag; C2H_TEST("Transform with binary operator", "[transform]") { const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 16))); const std::vector input1 = generate(num_items); const std::vector input2 = generate(num_items); const std::vector output(num_items, 0); pointer_t input1_ptr(input1); pointer_t input2_ptr(input2); pointer_t output_ptr(output); operation_t op = make_operation("op", R"(extern "C" __device__ void op(void* x_ptr, void* y_ptr, void* out_ptr ) { int* x = static_cast(x_ptr); int* y = static_cast(y_ptr); int* out = static_cast(out_ptr); *out = (*x > *y) ? *x : *y; })"); auto& build_cache = get_cache(); const auto& test_key = make_key(); binary_transform(input1_ptr, input2_ptr, output_ptr, num_items, op, build_cache, test_key); std::vector expected(num_items, 0); std::transform(input1.begin(), input1.end(), input2.begin(), expected.begin(), [](const int& x, const int& y) { return (x > y) ? x : y; }); if (num_items > 0) { REQUIRE(expected == std::vector(output_ptr)); } } struct alignas(16) binary_storage_in1 { long long a; int b; }; struct alignas(8) binary_storage_in2 { int c; int d; }; struct alignas(16) binary_storage_out { long long sum; int diff; bool operator==(const binary_storage_out& other) const { return sum == other.sum && diff == other.diff; } }; struct Transform_BinaryStorageTypes_Fixture_Tag; C2H_TEST("Transform works with binary storage types of different size/alignment", "[transform]") { const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 16))); operation_t op = make_operation("op", R"(struct alignas(16) binary_storage_in1 { long long a; int b; }; struct alignas(8) binary_storage_in2 { int c; int d; }; struct alignas(16) binary_storage_out { long long sum; int diff; }; extern "C" __device__ void op(void* x_ptr, void* y_ptr, void* out_ptr) { auto* x = static_cast(x_ptr); auto* y = static_cast(y_ptr); auto* out = static_cast(out_ptr); out->sum = x->a + static_cast(y->c); out->diff = x->b - y->d; })"); std::vector input1(num_items); std::vector input2(num_items); std::vector output(num_items); std::vector expected(num_items); for (std::size_t i = 0; i < num_items; ++i) { input1[i] = {static_cast(i + 5), static_cast(i + 2)}; input2[i] = {static_cast(i + 7), static_cast(i + 1)}; expected[i] = {input1[i].a + static_cast(input2[i].c), input1[i].b - input2[i].d}; } pointer_t input1_ptr(input1); pointer_t input2_ptr(input2); pointer_t output_ptr(output); auto& build_cache = get_cache(); const auto& test_key = make_key(); binary_transform(input1_ptr, input2_ptr, output_ptr, num_items, op, build_cache, test_key); if (num_items > 0) { REQUIRE(expected == std::vector(output_ptr)); } } struct Transform_BinaryOp_Iterator_Fixture_Tag; C2H_TEST("Binary transform with one iterator", "[transform]") { const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 16))); const std::vector input1 = generate(num_items); iterator_t> input2_it = make_counting_iterator("int"); input2_it.state.value = 0; const std::vector output(num_items, 0); pointer_t input1_ptr(input1); pointer_t output_ptr(output); operation_t op = make_operation("op", R"(extern "C" __device__ void op(void* x_ptr, void* y_ptr, void* out_ptr) { int* x = static_cast(x_ptr); int* y = static_cast(y_ptr); int* out = static_cast(out_ptr); *out = (*x > *y) ? *x : *y; })"); auto& build_cache = get_cache(); const auto& test_key = make_key(); binary_transform(input1_ptr, input2_it, output_ptr, num_items, op, build_cache, test_key); std::vector input2(num_items); std::iota(input2.begin(), input2.end(), 0); std::vector expected(num_items, 0); std::transform(input1.begin(), input1.end(), input2.begin(), expected.begin(), [](const int& x, const int& y) { return (x > y) ? x : y; }); if (num_items > 0) { REQUIRE(expected == std::vector(output_ptr)); } } using floating_point_types = c2h::type_list< #if _CCCL_HAS_NVFP16() __half, #endif float, double>; struct Transform_FloatingPointTypes_Fixture_Tag; C2H_TEST("Transform works with floating point types", "[transform]", floating_point_types) { using T = c2h::get<0, TestType>; const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 16))); operation_t op = make_operation("op", get_unary_op(get_type_info().type)); const std::vector int_input = generate(num_items); // Suppress harmless conversion warnings on MSVC _CCCL_DIAG_PUSH _CCCL_DIAG_SUPPRESS_MSVC(4244) const std::vector input(int_input.begin(), int_input.end()); _CCCL_DIAG_POP const std::vector output(num_items, 0); pointer_t input_ptr(input); pointer_t output_ptr(output); auto& build_cache = get_cache(); const auto& test_key = make_key(); unary_transform(input_ptr, output_ptr, num_items, op, build_cache, test_key); std::vector expected(num_items, 0); std::transform(input.begin(), input.end(), expected.begin(), [](const T& x) { return T{2} * x; }); if (num_items > 0) { REQUIRE(expected == std::vector(output_ptr)); } } C2H_TEST("Transform works with C++ source operations", "[transform]") { 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* input, void* output) { int* in = (int*)input; int* out = (int*)output; *out = *in * 2; } )"; 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(num_items); // 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 = fixture::get_or_create().get_value(); std::optional cache_opt = cache; unary_transform(input_ptr, output_ptr, num_items, op, cache_opt, test_key); const std::vector output = output_ptr; std::vector expected = input; std::transform(expected.begin(), expected.end(), expected.begin(), [](T x) { return x * 2; }); REQUIRE(output == expected); } C2H_TEST("Transform works with C++ source operations using custom headers", "[transform]") { 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* input, void* output) { int* in = (int*)input; int* out = (int*)output; int val = test_identity(*in); *out = val * 2; } )"; 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(num_items); // Test _ex version with custom build configuration const char* extra_flags[] = {"-DTEST_IDENTITY_ENABLED"}; const char* extra_dirs[] = {TEST_INCLUDE_PATH}; cccl_build_config config = make_build_config(extra_flags, 1, extra_dirs, 1); // Build with _ex version cccl_device_transform_build_result_t build{}; const auto& build_info = BuildInformation<>::init(); REQUIRE( CUDA_SUCCESS == cccl_device_unary_transform_build_ex( &build, input_ptr, output_ptr, op, 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(), &config)); // Execute the transform REQUIRE(CUDA_SUCCESS == cccl_device_unary_transform(build, input_ptr, output_ptr, num_items, op, CU_STREAM_LEGACY)); // Verify results std::vector output(num_items); cudaMemcpy(output.data(), static_cast(output_ptr.ptr), sizeof(T) * num_items, cudaMemcpyDeviceToHost); std::vector expected = input; std::transform(expected.begin(), expected.end(), expected.begin(), [](T x) { return x * 2; }); REQUIRE(output == expected); // Cleanup REQUIRE(CUDA_SUCCESS == cccl_device_transform_cleanup(&build)); } struct transform_stateful_counter_state_t { int* d_counter; }; C2H_TEST("Transform works with stateful unary operators", "[transform]") { const std::size_t num_items = GENERATE(0, 42, take(4, random(1 << 12, 1 << 16))); const std::vector host_counter{0}; pointer_t counter(host_counter); stateful_operation_t op = make_operation( "op", R"(struct transform_stateful_counter_state_t { int* d_counter; }; extern "C" __device__ void op(void* state_ptr, void* x_ptr, void* out_ptr) { auto* state = static_cast(state_ptr); atomicAdd(state->d_counter, 1); int x = *static_cast(x_ptr); *static_cast(out_ptr) = x * 2; })", transform_stateful_counter_state_t{counter.ptr}); const std::vector input = generate(num_items); const std::vector output(num_items, 0); pointer_t input_ptr(input); pointer_t output_ptr(output); std::optional build_cache = std::nullopt; std::optional test_key = std::nullopt; unary_transform(input_ptr, output_ptr, num_items, op, build_cache, test_key); std::vector expected(num_items, 0); std::transform(input.begin(), input.end(), expected.begin(), [](int x) { return x * 2; }); if (num_items > 0) { REQUIRE(expected == std::vector(output_ptr)); REQUIRE(counter[0] == static_cast(num_items)); } } #ifndef CCCL_C_PARALLEL_V2 C2H_TEST("Transform build result has serialization metadata populated", "[transform][serialization]") { using T = int32_t; constexpr int device_id = 0; const auto& build_info = BuildInformation::init(); cccl_op_t op = make_well_known_unary_operation(); pointer_t in(1); pointer_t out(1); BuildResultT build{}; REQUIRE( CUDA_SUCCESS == cccl_device_unary_transform_build( &build, in, out, op, 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.transform_kernel_lowered_name != nullptr); CHECK(build.transform_kernel_lowered_name[0] != '\0'); REQUIRE(CUDA_SUCCESS == cccl_device_transform_cleanup(&build)); } C2H_TEST("Transform compile/load round-trip", "[transform][serialization]") { using T = int32_t; constexpr int device_id = 0; const auto& build_info = BuildInformation::init(); cccl_op_t op = make_well_known_unary_operation(); pointer_t dummy_in(1); pointer_t dummy_out(1); BuildResultT build{}; REQUIRE( CUDA_SUCCESS == cccl_device_unary_transform_compile( &build, dummy_in, dummy_out, op, 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.transform_kernel_lowered_name != nullptr); CHECK(build.library == nullptr); CHECK(build.transform_kernel == nullptr); REQUIRE(CUDA_SUCCESS == cccl_device_transform_load(&build)); REQUIRE(build.library != nullptr); CHECK(build.transform_kernel != nullptr); constexpr std::size_t n = 16; const std::vector input = generate(n); pointer_t input_ptr(input); pointer_t output_ptr(n); CUstream null_stream = nullptr; REQUIRE(CUDA_SUCCESS == cccl_device_unary_transform(build, input_ptr, output_ptr, n, op, null_stream)); std::vector expected(input); std::transform(expected.begin(), expected.end(), expected.begin(), [](T x) { return -x; }); REQUIRE(expected == std::vector(output_ptr)); REQUIRE(CUDA_SUCCESS == cccl_device_transform_cleanup(&build)); } #endif // CCCL_C_PARALLEL_V2