[INFRA] Import NVIDIA/CCCL upstream as optimization reference library

CCCL (CUDA C++ Core Libraries) provides:
- CUB: device/block/warp-level GPU primitives (reduce, scan, sort, topk)
- Thrust: high-level parallel algorithms (transform_reduce, sort, scan)
- libcudacxx: CUDA C++ standard library (atomics, barriers, memory)
- cudax: experimental features (memory resources, allocators)
- Tuning policies: per-SM hardware-specific algorithm parameters

Competition optimization vectors mapped to CCCL:
- Output TPS (83% weight): warp_reduce, block_reduce, device_topk
- Input TPS (14% weight): device_scan, block_load, prefetch
- Cache TPS (3% weight): prefix caching strategy patterns
- Memory (0.9 util): pooled/cached/buddy allocators

Source: https://github.com/NVIDIA/cccl (shallow clone, HEAD only)
License: Apache-2.0
This commit is contained in:
EngineX CI
2026-07-30 09:35:51 +00:00
parent b4d01f481e
commit 56fd68e7dd
8871 changed files with 1454674 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
#===----------------------------------------------------------------------===##
#
# 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) 2026 NVIDIA CORPORATION & AFFILIATES.
#
#===----------------------------------------------------------------------===##
add_subdirectory(reduce)
add_subdirectory(exclusive_scan)
add_subdirectory(inclusive_scan)

View File

@@ -0,0 +1,20 @@
#===----------------------------------------------------------------------===##
#
# 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) 2026 NVIDIA CORPORATION & AFFILIATES.
#
#===----------------------------------------------------------------------===##
if (NOT cudax_ENABLE_NCCL)
return()
endif()
file(GLOB test_srcs LIST_DIRECTORIES FALSE CONFIGURE_DEPENDS *.cu *.cpp)
foreach (src IN LISTS test_srcs)
cudax_add_multi_gpu_test("algorithms.exclusive_scan" test_target "${src}")
target_link_libraries(${test_target} PRIVATE NCCL::nccl)
endforeach()

View File

@@ -0,0 +1,388 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/functional>
#include <cuda/memory_resource>
#include <cuda/std/array>
#include <cuda/std/cstddef>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/functional>
#include <cuda/std/limits>
#include <cuda/std/span>
#include <cuda/std/type_traits>
#include <cuda/experimental/__multi_gpu/algorithm/scan/scan.h>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
namespace
{
struct custom_plus
{
template <class T>
[[nodiscard]] _CCCL_HOST_DEVICE_API constexpr T operator()(const T& lhs, const T& rhs) const
{
return lhs + rhs;
}
};
using custom_value = c2h::custom_type_t<c2h::accumulateable_t, c2h::less_comparable_t, c2h::equal_comparable_t>;
using value_types = c2h::type_list<cuda::std::int32_t, float, custom_value>;
using operators = c2h::type_list<::cuda::std::plus<>, ::cuda::maximum<>, custom_plus>;
static_assert(cudax::nccl_transportable<custom_value>);
template <typename T>
T make_value(int i)
{
return static_cast<T>(i);
}
template <>
custom_value make_value<>(int i)
{
custom_value ret{};
ret.key = static_cast<std::size_t>(i);
ret.val = static_cast<std::size_t>(i);
return ret;
};
template <class T, class Op>
[[nodiscard]] T get_identity()
{
if constexpr (cuda::std::is_same_v<Op, cuda::std::plus<>> || cuda::std::is_same_v<Op, custom_plus>)
{
return make_value<T>(0);
}
else if constexpr (cuda::std::is_same_v<Op, cuda::maximum<>>)
{
return cuda::std::numeric_limits<T>::lowest();
}
else
{
static_assert(cuda::std::__always_false_v<T, Op>, "Add handling");
}
}
template <class T, class Op>
[[nodiscard]] std::vector<T>
expected_for_rank(int rank, const std::vector<std::vector<T>>& inputs_by_rank, const T& init, Op op)
{
std::vector<T> reference;
for (const auto& values : inputs_by_rank)
{
reference.insert(reference.end(), values.begin(), values.end());
}
std::vector<T> scan(reference.size());
std::exclusive_scan(reference.begin(), reference.end(), scan.begin(), init, op);
cuda::std::size_t offset = 0;
for (int r = 0; r < rank; ++r)
{
offset += inputs_by_rank[static_cast<cuda::std::size_t>(r)].size();
}
const auto count = inputs_by_rank[static_cast<cuda::std::size_t>(rank)].size();
return {scan.begin() + offset, scan.begin() + offset + count};
}
// Run the full scan, wait for it to finish, and check that `exclusive_scan` left its argument
// ranges untouched. This boilerplate is identical for every test regardless of how the inputs are
// shaped.
template <class Env, class T, class Op>
void do_exclusive_scan(
cuda::std::span<cudax::nccl_communicator_ref> comms,
const std::vector<Env>& envs,
std::vector<cuda::device_buffer<T>>& in,
std::vector<typename cuda::device_buffer<T>::iterator>& outputs,
const T& init,
const T& ident,
Op op)
{
const auto envs_size = envs.size();
const auto in_copy = in;
const auto outputs_copy = outputs;
INFO("init = " << init);
INFO("ident = " << ident);
cudax::exclusive_scan(cudax::distributed, comms, envs, in, outputs, init, op, ident);
// cuda::std::execution::env has no operator==, so we can only compare the sizes.
REQUIRE(envs.size() == envs_size);
// Scan call should not modify the inputs in any ways
REQUIRE(in.size() == in_copy.size());
for (cuda::std::size_t i = 0; i < in.size(); ++i)
{
INFO("device = " << i);
REQUIRE_THAT(in[i], Equals(in_copy[i]));
}
REQUIRE_THAT(outputs, Catch::Matchers::Equals(outputs_copy));
}
} // namespace
MULTI_GPU_TEST("exclusive_scan documentation example", c2h::type_list<int>)
{
auto comms = this->communicators();
if (comms.size() < 2)
{
SKIP("The exclusive_scan documentation example requires at least two local GPUs");
}
auto streams_owned = nccl_test_util::make_streams();
// Convert to stream_ref directly, cuda::stream on their own cant be passed directly to CUB
auto streams = std::vector<cuda::stream_ref>{streams_owned.begin(), streams_owned.end()};
//! [exclusive_scan]
constexpr cuda::std::array input_values{1, 2};
std::vector<cuda::device_buffer<int>> inputs;
std::vector<cuda::device_buffer<int>> outputs;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto device = comms[i].logical_device().underlying_device();
inputs.emplace_back(cuda::make_device_buffer<int>(streams[i], device, input_values));
outputs.emplace_back(cuda::make_device_buffer<int>(streams[i], device, input_values.size(), cuda::no_init));
}
std::vector<typename cuda::device_buffer<int>::iterator> output_iterators = make_output_iterators(outputs);
cudax::exclusive_scan(
cudax::distributed,
comms,
// Passing streams as the environment directly
streams,
inputs,
output_iterators,
/*__init=*/0);
constexpr cuda::std::array expected_rank_0{0, 1};
constexpr cuda::std::array expected_rank_1{3, 4};
const auto expected_0 =
cuda::make_buffer<int>(outputs[0].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_rank_0);
const auto expected_1 =
cuda::make_buffer<int>(outputs[1].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_rank_1);
REQUIRE_THAT(outputs[0], Equals(expected_0));
REQUIRE_THAT(outputs[1], Equals(expected_1));
//! [exclusive_scan]
}
MULTI_GPU_TEST("exclusive_scan, one element per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
// Seed each scan with a few hardcoded initializers. The init participates in the fold the same
// way on host and device, so any value works for every operator under test.
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Global rank `comms[i].rank()` contributes the single value `rank`. Each local rank also gets a
// one-element output buffer and an environment carrying its stream, so the scan is stream-ordered
// on the correct device. `reference` mirrors the contributions of every global rank so we can
// compute the host-side scan exactly like `exclusive_scan` does on the device.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
for (int r = 0; r < comms.front().size(); ++r)
{
inputs_by_rank[static_cast<cuda::std::size_t>(r)] = std::vector<T>(1, make_value<T>(r));
}
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto& values = inputs_by_rank[static_cast<cuda::std::size_t>(comms[i].rank())];
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), values.size(), cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_exclusive_scan(comms, envs, in, outputs, init, ident, Op{});
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_values = expected_for_rank<T>(comms[i].rank(), inputs_by_rank, init, Op{});
const auto exp = cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_values);
REQUIRE_THAT(out[i], Equals(exp));
}
}
MULTI_GPU_TEST("exclusive_scan, multiple elements per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
// Seed each scan with a few hardcoded initializers. The init participates in the fold the same
// way on host and device, so any value works for every operator under test.
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Global rank `comms[i].rank()` contributes ten copies of `rank`. `exclusive_scan` first
// computes a local prefix for each rank seeded by the prefix of all previous ranks. Each local
// rank also gets an output buffer and an environment carrying its stream. `reference` mirrors
// every global rank's ten contributions for the host-side scan.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (int r = 0; r < comms.front().size(); ++r)
{
inputs_by_rank[static_cast<cuda::std::size_t>(r)] = std::vector<T>(values_per_rank, make_value<T>(r));
}
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto& values = inputs_by_rank[static_cast<cuda::std::size_t>(comms[i].rank())];
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), values.size(), cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_exclusive_scan(comms, envs, in, outputs, init, ident, Op{});
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_values = expected_for_rank<T>(comms[i].rank(), inputs_by_rank, init, Op{});
const auto exp = cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_values);
REQUIRE_THAT(out[i], Equals(exp));
}
}
MULTI_GPU_TEST("exclusive_scan, some ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Even global ranks contribute ten copies of `rank`; odd global ranks contribute an empty input
// range. Rank 0 is always non-empty. `exclusive_scan` must treat an empty rank as contributing
// nothing, exactly like `std::exclusive_scan` over the surviving elements. `reference` mirrors
// that for the host-side scan.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (int r = 0; r < comms.front().size(); ++r)
{
if (r % 2 == 0)
{
inputs_by_rank[static_cast<cuda::std::size_t>(r)] = std::vector<T>(values_per_rank, make_value<T>(r));
}
}
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto& values = inputs_by_rank[static_cast<cuda::std::size_t>(comms[i].rank())];
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), values.size(), cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_exclusive_scan(comms, envs, in, outputs, init, ident, Op{});
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_values = expected_for_rank<T>(comms[i].rank(), inputs_by_rank, init, Op{});
const auto exp = cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_values);
REQUIRE_THAT(out[i], Equals(exp));
}
}
MULTI_GPU_TEST("exclusive_scan, all ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const auto init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// No rank contributes any element. Scanning nothing produces no output values, exactly like
// `std::exclusive_scan` over an empty range.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto& values = inputs_by_rank[static_cast<cuda::std::size_t>(comms[i].rank())];
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), values.size(), cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_exclusive_scan(comms, envs, in, outputs, init, ident, Op{});
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_values = expected_for_rank<T>(comms[i].rank(), inputs_by_rank, init, Op{});
const auto exp = cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_values);
REQUIRE_THAT(out[i], Equals(exp));
}
}

View File

@@ -0,0 +1,112 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/functional>
#include <cuda/memory_resource>
#include <cuda/std/cstddef>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/functional>
#include <cuda/std/iterator>
#include <cuda/experimental/__multi_gpu/algorithm/scan/scan.h>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
MULTI_GPU_TEST("exclusive_scan, range overloads default values", )
{
using T = cuda::std::int32_t;
using Op = ::cuda::std::plus<>;
constexpr auto init = T{};
constexpr T ident = cuda::identity_element<Op, T>();
constexpr auto op = Op{};
const auto comms = this->communicators();
const auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<cuda::stream_ref> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
std::vector<T> values(values_per_rank);
std::iota(values.begin(), values.end(), static_cast<T>(comms[i].rank() * values_per_rank + 1));
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), cuda::std::size(values), cuda::no_init));
envs.emplace_back(streams[i]);
}
auto outputs = make_output_iterators(out);
const auto expected_values = [&] {
std::vector<T> reference(static_cast<cuda::std::size_t>(comms.front().size()) * values_per_rank);
std::iota(reference.begin(), reference.end(), T{1});
std::vector<T> expected_values(reference.size());
std::exclusive_scan(reference.begin(), reference.end(), expected_values.begin(), init, op);
return expected_values;
}();
const auto check_outputs = [&] {
const auto exp_span = cuda::std::span{expected_values};
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_for_rank = exp_span.subspan(comms[i].rank() * values_per_rank, values_per_rank);
const auto expected =
cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_for_rank);
REQUIRE_THAT(out[i], Equals(expected));
}
};
SECTION("Default init, op, ident (all)")
{
cudax::exclusive_scan(cudax::distributed, comms, envs, in, outputs);
check_outputs();
}
SECTION("Default op, ident")
{
cudax::exclusive_scan(cudax::distributed, comms, envs, in, outputs, init);
check_outputs();
}
SECTION("Default ident")
{
cudax::exclusive_scan(cudax::distributed, comms, envs, in, outputs, init, op);
check_outputs();
}
SECTION("Default none")
{
cudax::exclusive_scan(cudax::distributed, comms, envs, in, outputs, init, op, ident);
check_outputs();
}
}

View File

@@ -0,0 +1,289 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/functional>
#include <cuda/memory_resource>
#include <cuda/std/array>
#include <cuda/std/cstddef>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/functional>
#include <cuda/std/limits>
#include <cuda/std/span>
#include <cuda/std/type_traits>
#include <cuda/experimental/__multi_gpu/algorithm/scan/scan.h>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
namespace
{
struct custom_plus
{
template <class T>
[[nodiscard]] _CCCL_HOST_DEVICE_API constexpr T operator()(const T& lhs, const T& rhs) const
{
return lhs + rhs;
}
};
using custom_value = c2h::custom_type_t<c2h::accumulateable_t, c2h::less_comparable_t, c2h::equal_comparable_t>;
using value_types = c2h::type_list<cuda::std::int32_t, float, custom_value>;
using operators = c2h::type_list<::cuda::std::plus<>, ::cuda::maximum<>, custom_plus>;
static_assert(cudax::nccl_transportable<custom_value>);
template <typename T>
T make_value(int i)
{
return static_cast<T>(i);
}
template <>
custom_value make_value<>(int i)
{
custom_value ret{};
ret.key = static_cast<std::size_t>(i);
ret.val = static_cast<std::size_t>(i);
return ret;
};
template <class T, class Op>
[[nodiscard]] T get_identity()
{
if constexpr (cuda::std::is_same_v<Op, cuda::std::plus<>> || cuda::std::is_same_v<Op, custom_plus>)
{
return make_value<T>(0);
}
else if constexpr (cuda::std::is_same_v<Op, cuda::maximum<>>)
{
return cuda::std::numeric_limits<T>::lowest();
}
else
{
static_assert(cuda::std::__always_false_v<T, Op>, "Add handling");
}
}
template <class T, class Op>
[[nodiscard]] std::vector<T>
expected_for_rank(int rank, const std::vector<std::vector<T>>& inputs_by_rank, const T& init, Op op)
{
std::vector<T> reference;
for (const auto& values : inputs_by_rank)
{
reference.insert(reference.end(), values.begin(), values.end());
}
std::vector<T> scan(reference.size());
std::exclusive_scan(reference.begin(), reference.end(), scan.begin(), init, op);
cuda::std::size_t offset = 0;
for (int r = 0; r < rank; ++r)
{
offset += inputs_by_rank[static_cast<cuda::std::size_t>(r)].size();
}
const auto count = inputs_by_rank[static_cast<cuda::std::size_t>(rank)].size();
return {scan.begin() + offset, scan.begin() + offset + count};
}
// Drive the scan through the single-communicator overload, one thread per local rank. The
// per-rank calls must rendezvous in their collectives, so issuing them serially would deadlock.
// Catch2 assertions remain on the main thread after all worker threads have joined.
template <class T, class Op>
void run_case(cuda::std::span<cudax::nccl_communicator_ref> comms,
const std::vector<std::vector<T>>& inputs_by_rank,
const T& init,
const T& ident,
Op op)
{
auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto& values = inputs_by_rank[static_cast<cuda::std::size_t>(comms[i].rank())];
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), values.size(), cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
const auto in_copy = in;
auto outputs = make_output_iterators(out);
const auto outputs_copy = outputs;
INFO("init = " << init);
INFO("ident = " << ident);
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::exclusive_scan(cudax::distributed, comms[i], envs[i], in[i], outputs[i], init, op, ident);
});
REQUIRE(in.size() == in_copy.size());
for (cuda::std::size_t i = 0; i < in.size(); ++i)
{
INFO("device = " << i);
REQUIRE_THAT(in[i], Equals(in_copy[i]));
const auto expected_values = expected_for_rank<T>(comms[i].rank(), inputs_by_rank, init, op);
const auto expected =
cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_values);
REQUIRE_THAT(out[i], Equals(expected));
}
REQUIRE_THAT(outputs, Catch::Matchers::Equals(outputs_copy));
}
} // namespace
MULTI_GPU_TEST("exclusive_scan single-comm documentation example", c2h::type_list<int>)
{
auto comms = this->communicators();
if (comms.size() < 2)
{
SKIP("The exclusive_scan documentation example requires at least two local GPUs");
}
auto streams_owned = nccl_test_util::make_streams();
auto streams = std::vector<cuda::stream_ref>{streams_owned.begin(), streams_owned.end()};
// Must be pre-allocated since it is written to by threads
std::vector<std::string> failed(comms.front().size());
// Every communicator rank must invoke the collective concurrently.
run_threaded(comms.size(), [&](cuda::std::size_t i) {
auto& communicator = comms[i];
auto environment = streams[i];
const auto device = communicator.logical_device().underlying_device();
//! [exclusive_scan_single_range]
constexpr cuda::std::array input_values{1, 2};
auto input = cuda::make_device_buffer<int>(environment, device, input_values);
auto output = cuda::make_device_buffer<int>(environment, device, input_values.size(), cuda::no_init);
cudax::exclusive_scan(cudax::distributed, communicator, environment, input, output.begin(), /*__init=*/0);
// Every rank contributes {1, 2}, so rank r starts with a prefix of 3 * r.
const auto rank = communicator.rank();
const auto expected =
cuda::make_buffer<int>(output.stream(), cuda::mr::legacy_pinned_memory_resource{}, {3 * rank, 3 * rank + 1});
//! [exclusive_scan_single_range]
// catch2 isn't thread safe by default, so we can't use the usual requires expression. So
// we roll a hacky version of it ourselves
if (const auto matcher = Equals(expected); !matcher.match(output))
{
failed[rank] = matcher.describe();
}
});
for (cuda::std::size_t i = 0; i < failed.size(); ++i)
{
if (const auto& err_str = failed[i]; !err_str.empty())
{
INFO("rank: " << i);
REQUIRE(err_str == ""); // Should print the full error string
}
}
}
MULTI_GPU_TEST("exclusive_scan single-comm, one element per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
std::vector<std::vector<T>> inputs_by_rank;
inputs_by_rank.reserve(static_cast<cuda::std::size_t>(comms.front().size()));
for (int r = 0; r < comms.front().size(); ++r)
{
const auto v = {make_value<T>(r)};
inputs_by_rank.emplace_back(v);
}
run_case(comms, inputs_by_rank, init, ident, Op{});
}
MULTI_GPU_TEST("exclusive_scan single-comm, multiple elements per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
constexpr auto values_per_rank = 10;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
for (int r = 0; r < comms.front().size(); ++r)
{
inputs_by_rank[static_cast<cuda::std::size_t>(r)] = std::vector<T>(values_per_rank, make_value<T>(r));
}
run_case(comms, inputs_by_rank, init, ident, Op{});
}
MULTI_GPU_TEST("exclusive_scan single-comm, some ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
constexpr auto values_per_rank = 10;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
for (int r = 0; r < comms.front().size(); ++r)
{
if (r % 2 == 0)
{
inputs_by_rank[static_cast<cuda::std::size_t>(r)] = std::vector<T>(values_per_rank, make_value<T>(r));
}
}
run_case(comms, inputs_by_rank, init, ident, Op{});
}
MULTI_GPU_TEST("exclusive_scan single-comm, all ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
const std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
run_case(comms, inputs_by_rank, init, ident, Op{});
}

View File

@@ -0,0 +1,120 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/functional>
#include <cuda/memory_resource>
#include <cuda/std/cstddef>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/functional>
#include <cuda/std/iterator>
#include <cuda/experimental/__multi_gpu/algorithm/scan/scan.h>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
MULTI_GPU_TEST("exclusive_scan single-comm, overloads default values", )
{
using T = cuda::std::int32_t;
using Op = ::cuda::std::plus<>;
constexpr auto init = T{};
constexpr T ident = cuda::identity_element<Op, T>();
constexpr auto op = Op{};
const auto comms = this->communicators();
const auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<cuda::stream_ref> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
std::vector<T> values(values_per_rank);
std::iota(values.begin(), values.end(), static_cast<T>(comms[i].rank() * values_per_rank + 1));
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), cuda::std::size(values), cuda::no_init));
envs.emplace_back(streams[i]);
}
auto outputs = make_output_iterators(out);
const auto expected_values = [&] {
std::vector<T> reference(static_cast<cuda::std::size_t>(comms.front().size()) * values_per_rank);
std::iota(reference.begin(), reference.end(), T{1});
std::vector<T> expected_values(reference.size());
std::exclusive_scan(reference.begin(), reference.end(), expected_values.begin(), init, op);
return expected_values;
}();
const auto check_outputs = [&] {
const auto exp_span = cuda::std::span{expected_values};
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_for_rank = exp_span.subspan(comms[i].rank() * values_per_rank, values_per_rank);
const auto expected =
cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_for_rank);
REQUIRE_THAT(out[i], Equals(expected));
}
};
SECTION("Default init, op, ident (all)")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::exclusive_scan(cudax::distributed, comms[i], envs[i], in[i], outputs[i]);
});
check_outputs();
}
SECTION("Default op, ident")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::exclusive_scan(cudax::distributed, comms[i], envs[i], in[i], outputs[i], init);
});
check_outputs();
}
SECTION("Default ident")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::exclusive_scan(cudax::distributed, comms[i], envs[i], in[i], outputs[i], init, op);
});
check_outputs();
}
SECTION("Default none")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::exclusive_scan(cudax::distributed, comms[i], envs[i], in[i], outputs[i], init, op, ident);
});
check_outputs();
}
}

View File

@@ -0,0 +1,20 @@
#===----------------------------------------------------------------------===##
#
# 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) 2026 NVIDIA CORPORATION & AFFILIATES.
#
#===----------------------------------------------------------------------===##
if (NOT cudax_ENABLE_NCCL)
return()
endif()
file(GLOB test_srcs LIST_DIRECTORIES FALSE CONFIGURE_DEPENDS *.cu *.cpp)
foreach (src IN LISTS test_srcs)
cudax_add_multi_gpu_test("algorithms.inclusive_scan" test_target "${src}")
target_link_libraries(${test_target} PRIVATE NCCL::nccl)
endforeach()

View File

@@ -0,0 +1,389 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/functional>
#include <cuda/memory_resource>
#include <cuda/std/array>
#include <cuda/std/cstddef>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/functional>
#include <cuda/std/limits>
#include <cuda/std/span>
#include <cuda/std/type_traits>
#include <cuda/experimental/__multi_gpu/algorithm/scan/scan.h>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
namespace
{
struct custom_plus
{
template <class T>
[[nodiscard]] _CCCL_HOST_DEVICE_API constexpr T operator()(const T& lhs, const T& rhs) const
{
return lhs + rhs;
}
};
using custom_value = c2h::custom_type_t<c2h::accumulateable_t, c2h::less_comparable_t, c2h::equal_comparable_t>;
using value_types = c2h::type_list<cuda::std::int32_t, float, custom_value>;
using operators = c2h::type_list<::cuda::std::plus<>, ::cuda::maximum<>, custom_plus>;
static_assert(cudax::nccl_transportable<custom_value>);
template <typename T>
T make_value(int i)
{
return static_cast<T>(i);
}
template <>
custom_value make_value<>(int i)
{
custom_value ret{};
ret.key = static_cast<std::size_t>(i);
ret.val = static_cast<std::size_t>(i);
return ret;
};
template <class T, class Op>
[[nodiscard]] T get_identity()
{
if constexpr (cuda::std::is_same_v<Op, cuda::std::plus<>> || cuda::std::is_same_v<Op, custom_plus>)
{
return make_value<T>(0);
}
else if constexpr (cuda::std::is_same_v<Op, cuda::maximum<>>)
{
return cuda::std::numeric_limits<T>::lowest();
}
else
{
static_assert(cuda::std::__always_false_v<T, Op>, "Add handling");
}
}
template <class T, class Op>
[[nodiscard]] std::vector<T>
expected_for_rank(int rank, const std::vector<std::vector<T>>& inputs_by_rank, const T& init, Op op)
{
std::vector<T> reference;
for (const auto& values : inputs_by_rank)
{
reference.insert(reference.end(), values.begin(), values.end());
}
std::vector<T> scan(reference.size());
std::inclusive_scan(reference.begin(), reference.end(), scan.begin(), op, init);
cuda::std::size_t offset = 0;
for (int r = 0; r < rank; ++r)
{
offset += inputs_by_rank[static_cast<cuda::std::size_t>(r)].size();
}
const auto count = inputs_by_rank[static_cast<cuda::std::size_t>(rank)].size();
return {scan.begin() + offset, scan.begin() + offset + count};
}
// Run the full scan, wait for it to finish, and check that `inclusive_scan` left its argument
// ranges untouched. This boilerplate is identical for every test regardless of how the inputs are
// shaped.
template <class Env, class T, class Op>
void do_inclusive_scan(
cuda::std::span<cudax::nccl_communicator_ref> comms,
const std::vector<Env>& envs,
std::vector<cuda::device_buffer<T>>& in,
std::vector<typename cuda::device_buffer<T>::iterator>& outputs,
const T& init,
const T& ident,
Op op)
{
const auto envs_size = envs.size();
const auto in_copy = in;
const auto outputs_copy = outputs;
INFO("init = " << init);
INFO("ident = " << ident);
cudax::inclusive_scan(cudax::distributed, comms, envs, in, outputs, init, op, ident);
// cuda::std::execution::env has no operator==, so we can only compare the sizes.
REQUIRE(envs.size() == envs_size);
// Scan call should not modify the inputs in any ways
REQUIRE(in.size() == in_copy.size());
for (cuda::std::size_t i = 0; i < in.size(); ++i)
{
INFO("device = " << i);
REQUIRE_THAT(in[i], Equals(in_copy[i]));
}
REQUIRE_THAT(outputs, Catch::Matchers::Equals(outputs_copy));
}
} // namespace
MULTI_GPU_TEST("inclusive_scan documentation example", c2h::type_list<int>)
{
auto comms = this->communicators();
if (comms.size() < 2)
{
SKIP("The inclusive_scan documentation example requires at least two local GPUs");
}
auto streams_owned = nccl_test_util::make_streams();
// Convert to stream_ref directly, cuda::stream on their own cant be passed directly to CUB
auto streams = std::vector<cuda::stream_ref>{streams_owned.begin(), streams_owned.end()};
//! [inclusive_scan]
constexpr cuda::std::array input_values{1, 2};
std::vector<cuda::device_buffer<int>> inputs;
std::vector<cuda::device_buffer<int>> outputs;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto device = comms[i].logical_device().underlying_device();
inputs.emplace_back(cuda::make_device_buffer<int>(streams[i], device, input_values));
outputs.emplace_back(cuda::make_device_buffer<int>(streams[i], device, input_values.size(), cuda::no_init));
}
std::vector<typename cuda::device_buffer<int>::iterator> output_iterators = make_output_iterators(outputs);
cudax::inclusive_scan(
cudax::distributed,
comms,
// Passing streams as the environment directly
streams,
inputs,
output_iterators,
/*__init=*/0);
constexpr cuda::std::array expected_rank_0{1, 3};
constexpr cuda::std::array expected_rank_1{4, 6};
const auto expected_0 =
cuda::make_buffer<int>(outputs[0].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_rank_0);
const auto expected_1 =
cuda::make_buffer<int>(outputs[1].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_rank_1);
REQUIRE_THAT(outputs[0], Equals(expected_0));
REQUIRE_THAT(outputs[1], Equals(expected_1));
//! [inclusive_scan]
}
MULTI_GPU_TEST("inclusive_scan, one element per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
// Seed each scan with a few hardcoded initializers. The init participates in the fold the same
// way on host and device, so any value works for every operator under test.
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Global rank `comms[i].rank()` contributes the single value `rank`. Each local rank also gets a
// one-element output buffer and an environment carrying its stream, so the scan is stream-ordered
// on the correct device. `reference` mirrors the contributions of every global rank so we can
// compute the host-side scan exactly like `inclusive_scan` does on the device.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
for (int r = 0; r < comms.front().size(); ++r)
{
inputs_by_rank[static_cast<cuda::std::size_t>(r)] = std::vector<T>(1, make_value<T>(r));
}
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto& values = inputs_by_rank[static_cast<cuda::std::size_t>(comms[i].rank())];
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), values.size(), cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_inclusive_scan(comms, envs, in, outputs, init, ident, Op{});
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_values = expected_for_rank<T>(comms[i].rank(), inputs_by_rank, init, Op{});
const auto exp = cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_values);
REQUIRE_THAT(out[i], Equals(exp));
}
}
MULTI_GPU_TEST("inclusive_scan, multiple elements per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
// Seed each scan with a few hardcoded initializers. The init participates in the fold the same
// way on host and device, so any value works for every operator under test.
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Global rank `comms[i].rank()` contributes ten copies of `rank`. `inclusive_scan` first
// computes a local prefix for each rank seeded by the prefix of all previous ranks. Each local
// rank also gets an output buffer and an environment carrying its stream. `reference` mirrors
// every global rank's ten contributions for the host-side scan.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (int r = 0; r < comms.front().size(); ++r)
{
const auto v = make_value<T>(r);
inputs_by_rank[static_cast<cuda::std::size_t>(r)] = std::vector<T>(values_per_rank, v);
}
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto& values = inputs_by_rank[static_cast<cuda::std::size_t>(comms[i].rank())];
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), values.size(), cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_inclusive_scan(comms, envs, in, outputs, init, ident, Op{});
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_values = expected_for_rank<T>(comms[i].rank(), inputs_by_rank, init, Op{});
const auto exp = cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_values);
REQUIRE_THAT(out[i], Equals(exp));
}
}
MULTI_GPU_TEST("inclusive_scan, some ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Even global ranks contribute ten copies of `rank`; odd global ranks contribute an empty input
// range. Rank 0 is always non-empty. `inclusive_scan` must treat an empty rank as contributing
// nothing, exactly like `std::inclusive_scan` over the surviving elements. `reference` mirrors
// that for the host-side scan.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (int r = 0; r < comms.front().size(); ++r)
{
if (r % 2 == 0)
{
inputs_by_rank[static_cast<cuda::std::size_t>(r)] = std::vector<T>(values_per_rank, make_value<T>(r));
}
}
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto& values = inputs_by_rank[static_cast<cuda::std::size_t>(comms[i].rank())];
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), values.size(), cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_inclusive_scan(comms, envs, in, outputs, init, ident, Op{});
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_values = expected_for_rank<T>(comms[i].rank(), inputs_by_rank, init, Op{});
const auto exp = cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_values);
REQUIRE_THAT(out[i], Equals(exp));
}
}
MULTI_GPU_TEST("inclusive_scan, all ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const auto init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// No rank contributes any element. Scanning nothing produces no output values, exactly like
// `std::inclusive_scan` over an empty range.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto& values = inputs_by_rank[static_cast<cuda::std::size_t>(comms[i].rank())];
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), values.size(), cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_inclusive_scan(comms, envs, in, outputs, init, ident, Op{});
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_values = expected_for_rank<T>(comms[i].rank(), inputs_by_rank, init, Op{});
const auto exp = cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_values);
REQUIRE_THAT(out[i], Equals(exp));
}
}

View File

@@ -0,0 +1,112 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/functional>
#include <cuda/memory_resource>
#include <cuda/std/cstddef>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/functional>
#include <cuda/std/iterator>
#include <cuda/experimental/__multi_gpu/algorithm/scan/scan.h>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
MULTI_GPU_TEST("inclusive_scan, range overloads default values", )
{
using T = cuda::std::int32_t;
using Op = ::cuda::std::plus<>;
constexpr auto init = T{};
constexpr T ident = cuda::identity_element<Op, T>();
constexpr auto op = Op{};
const auto comms = this->communicators();
const auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<cuda::stream_ref> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
std::vector<T> values(values_per_rank);
std::iota(values.begin(), values.end(), static_cast<T>(comms[i].rank() * values_per_rank + 1));
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), cuda::std::size(values), cuda::no_init));
envs.emplace_back(streams[i]);
}
auto outputs = make_output_iterators(out);
const auto expected_values = [&] {
std::vector<T> reference(static_cast<cuda::std::size_t>(comms.front().size()) * values_per_rank);
std::iota(reference.begin(), reference.end(), T{1});
std::vector<T> expected_values(reference.size());
std::inclusive_scan(reference.begin(), reference.end(), expected_values.begin(), op, init);
return expected_values;
}();
const auto check_outputs = [&] {
const auto exp_span = cuda::std::span{expected_values};
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_for_rank = exp_span.subspan(comms[i].rank() * values_per_rank, values_per_rank);
const auto expected =
cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_for_rank);
REQUIRE_THAT(out[i], Equals(expected));
}
};
SECTION("Default init, op, ident (all)")
{
cudax::inclusive_scan(cudax::distributed, comms, envs, in, outputs);
check_outputs();
}
SECTION("Default op, ident")
{
cudax::inclusive_scan(cudax::distributed, comms, envs, in, outputs, init);
check_outputs();
}
SECTION("Default ident")
{
cudax::inclusive_scan(cudax::distributed, comms, envs, in, outputs, init, op);
check_outputs();
}
SECTION("Default none")
{
cudax::inclusive_scan(cudax::distributed, comms, envs, in, outputs, init, op, ident);
check_outputs();
}
}

View File

@@ -0,0 +1,290 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/functional>
#include <cuda/memory_resource>
#include <cuda/std/array>
#include <cuda/std/cstddef>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/functional>
#include <cuda/std/limits>
#include <cuda/std/span>
#include <cuda/std/type_traits>
#include <cuda/experimental/__multi_gpu/algorithm/scan/scan.h>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
namespace
{
struct custom_plus
{
template <class T>
[[nodiscard]] _CCCL_HOST_DEVICE_API constexpr T operator()(const T& lhs, const T& rhs) const
{
return lhs + rhs;
}
};
using custom_value = c2h::custom_type_t<c2h::accumulateable_t, c2h::less_comparable_t, c2h::equal_comparable_t>;
using value_types = c2h::type_list<cuda::std::int32_t, float, custom_value>;
using operators = c2h::type_list<::cuda::std::plus<>, ::cuda::maximum<>, custom_plus>;
static_assert(cudax::nccl_transportable<custom_value>);
template <typename T>
T make_value(int i)
{
return static_cast<T>(i);
}
template <>
custom_value make_value<>(int i)
{
custom_value ret{};
ret.key = static_cast<std::size_t>(i);
ret.val = static_cast<std::size_t>(i);
return ret;
};
template <class T, class Op>
[[nodiscard]] T get_identity()
{
if constexpr (cuda::std::is_same_v<Op, cuda::std::plus<>> || cuda::std::is_same_v<Op, custom_plus>)
{
return make_value<T>(0);
}
else if constexpr (cuda::std::is_same_v<Op, cuda::maximum<>>)
{
return cuda::std::numeric_limits<T>::lowest();
}
else
{
static_assert(cuda::std::__always_false_v<T, Op>, "Add handling");
}
}
template <class T, class Op>
[[nodiscard]] std::vector<T>
expected_for_rank(int rank, const std::vector<std::vector<T>>& inputs_by_rank, const T& init, Op op)
{
std::vector<T> reference;
for (const auto& values : inputs_by_rank)
{
reference.insert(reference.end(), values.begin(), values.end());
}
std::vector<T> scan(reference.size());
std::inclusive_scan(reference.begin(), reference.end(), scan.begin(), op, init);
cuda::std::size_t offset = 0;
for (int r = 0; r < rank; ++r)
{
offset += inputs_by_rank[static_cast<cuda::std::size_t>(r)].size();
}
const auto count = inputs_by_rank[static_cast<cuda::std::size_t>(rank)].size();
return {scan.begin() + offset, scan.begin() + offset + count};
}
// Drive the scan through the single-communicator overload, one thread per local rank. The
// per-rank calls must rendezvous in their collectives, so issuing them serially would deadlock.
// Catch2 assertions remain on the main thread after all worker threads have joined.
template <class T, class Op>
void run_case(cuda::std::span<cudax::nccl_communicator_ref> comms,
const std::vector<std::vector<T>>& inputs_by_rank,
const T& init,
const T& ident,
Op op)
{
auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto& values = inputs_by_rank[static_cast<cuda::std::size_t>(comms[i].rank())];
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), values.size(), cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
const auto in_copy = in;
auto outputs = make_output_iterators(out);
const auto outputs_copy = outputs;
INFO("init = " << init);
INFO("ident = " << ident);
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::inclusive_scan(cudax::distributed, comms[i], envs[i], in[i], outputs[i], init, op, ident);
});
REQUIRE(in.size() == in_copy.size());
for (cuda::std::size_t i = 0; i < in.size(); ++i)
{
INFO("device = " << i);
REQUIRE_THAT(in[i], Equals(in_copy[i]));
const auto expected_values = expected_for_rank<T>(comms[i].rank(), inputs_by_rank, init, op);
const auto expected =
cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_values);
REQUIRE_THAT(out[i], Equals(expected));
}
REQUIRE_THAT(outputs, Catch::Matchers::Equals(outputs_copy));
}
} // namespace
MULTI_GPU_TEST("inclusive_scan single-comm documentation example", c2h::type_list<int>)
{
auto comms = this->communicators();
if (comms.size() < 2)
{
SKIP("The inclusive_scan documentation example requires at least two local GPUs");
}
auto streams_owned = nccl_test_util::make_streams();
auto streams = std::vector<cuda::stream_ref>{streams_owned.begin(), streams_owned.end()};
// Must be pre-allocated since it is written to by threads
std::vector<std::string> failed(comms.front().size());
// Every communicator rank must invoke the collective concurrently.
run_threaded(comms.size(), [&](cuda::std::size_t i) {
auto& communicator = comms[i];
auto environment = streams[i];
const auto device = communicator.logical_device().underlying_device();
//! [inclusive_scan_single_range]
constexpr cuda::std::array input_values{1, 2};
auto input = cuda::make_device_buffer<int>(environment, device, input_values);
auto output = cuda::make_device_buffer<int>(environment, device, input_values.size(), cuda::no_init);
cudax::inclusive_scan(cudax::distributed, communicator, environment, input, output.begin(), /*__init=*/0);
// Every rank contributes {1, 2}, so rank r starts with a prefix of 3 * r.
const auto rank = communicator.rank();
const auto expected =
cuda::make_buffer<int>(output.stream(), cuda::mr::legacy_pinned_memory_resource{}, {3 * rank + 1, 3 * rank + 3});
//! [inclusive_scan_single_range]
// catch2 isn't thread safe by default, so we can't use the usual requires expression. So
// we roll a hacky version of it ourselves
if (const auto matcher = Equals(expected); !matcher.match(output))
{
failed[rank] = matcher.describe();
}
});
for (cuda::std::size_t i = 0; i < failed.size(); ++i)
{
if (const auto& err_str = failed[i]; !err_str.empty())
{
INFO("rank: " << i);
REQUIRE(err_str == ""); // Should print the full error string
}
}
}
MULTI_GPU_TEST("inclusive_scan single-comm, one element per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
std::vector<std::vector<T>> inputs_by_rank;
inputs_by_rank.reserve(static_cast<cuda::std::size_t>(comms.front().size()));
for (int r = 0; r < comms.front().size(); ++r)
{
const auto v = {make_value<T>(r)};
inputs_by_rank.emplace_back(v);
}
run_case(comms, inputs_by_rank, init, ident, Op{});
}
MULTI_GPU_TEST("inclusive_scan single-comm, multiple elements per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
constexpr auto values_per_rank = 10;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
for (int r = 0; r < comms.front().size(); ++r)
{
const auto value = make_value<T>(r);
inputs_by_rank[static_cast<cuda::std::size_t>(r)] = std::vector<T>(values_per_rank, value);
}
run_case(comms, inputs_by_rank, init, ident, Op{});
}
MULTI_GPU_TEST("inclusive_scan single-comm, some ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
constexpr auto values_per_rank = 10;
std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
for (int r = 0; r < comms.front().size(); ++r)
{
if (r % 2 == 0)
{
inputs_by_rank[static_cast<cuda::std::size_t>(r)] = std::vector<T>(values_per_rank, make_value<T>(r));
}
}
run_case(comms, inputs_by_rank, init, ident, Op{});
}
MULTI_GPU_TEST("inclusive_scan single-comm, all ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
const std::vector<std::vector<T>> inputs_by_rank(static_cast<cuda::std::size_t>(comms.front().size()));
run_case(comms, inputs_by_rank, init, ident, Op{});
}

View File

@@ -0,0 +1,120 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/functional>
#include <cuda/memory_resource>
#include <cuda/std/cstddef>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/functional>
#include <cuda/std/iterator>
#include <cuda/experimental/__multi_gpu/algorithm/scan/scan.h>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
MULTI_GPU_TEST("inclusive_scan single-comm, overloads default values", )
{
using T = cuda::std::int32_t;
using Op = ::cuda::std::plus<>;
constexpr auto init = T{};
constexpr T ident = cuda::identity_element<Op, T>();
constexpr auto op = Op{};
const auto comms = this->communicators();
const auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<cuda::stream_ref> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
std::vector<T> values(values_per_rank);
std::iota(values.begin(), values.end(), static_cast<T>(comms[i].rank() * values_per_rank + 1));
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(cuda::make_device_buffer<T>(
streams[i], comms[i].logical_device().underlying_device(), cuda::std::size(values), cuda::no_init));
envs.emplace_back(streams[i]);
}
auto outputs = make_output_iterators(out);
const auto expected_values = [&] {
std::vector<T> reference(static_cast<cuda::std::size_t>(comms.front().size()) * values_per_rank);
std::iota(reference.begin(), reference.end(), T{1});
std::vector<T> expected_values(reference.size());
std::inclusive_scan(reference.begin(), reference.end(), expected_values.begin(), op, init);
return expected_values;
}();
const auto check_outputs = [&] {
const auto exp_span = cuda::std::span{expected_values};
for (cuda::std::size_t i = 0; i < out.size(); ++i)
{
const auto expected_for_rank = exp_span.subspan(comms[i].rank() * values_per_rank, values_per_rank);
const auto expected =
cuda::make_buffer<T>(out[i].stream(), cuda::mr::legacy_pinned_memory_resource{}, expected_for_rank);
REQUIRE_THAT(out[i], Equals(expected));
}
};
SECTION("Default init, op, ident (all)")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::inclusive_scan(cudax::distributed, comms[i], envs[i], in[i], outputs[i]);
});
check_outputs();
}
SECTION("Default op, ident")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::inclusive_scan(cudax::distributed, comms[i], envs[i], in[i], outputs[i], init);
});
check_outputs();
}
SECTION("Default ident")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::inclusive_scan(cudax::distributed, comms[i], envs[i], in[i], outputs[i], init, op);
});
check_outputs();
}
SECTION("Default none")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::inclusive_scan(cudax::distributed, comms[i], envs[i], in[i], outputs[i], init, op, ident);
});
check_outputs();
}
}

View File

@@ -0,0 +1,20 @@
#===----------------------------------------------------------------------===##
#
# 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) 2026 NVIDIA CORPORATION & AFFILIATES.
#
#===----------------------------------------------------------------------===##
if (NOT cudax_ENABLE_NCCL)
return()
endif()
file(GLOB test_srcs LIST_DIRECTORIES FALSE CONFIGURE_DEPENDS *.cu *.cpp)
foreach (src IN LISTS test_srcs)
cudax_add_multi_gpu_test("algorithms.reduce" test_target "${src}")
target_link_libraries(${test_target} PRIVATE NCCL::nccl)
endforeach()

View File

@@ -0,0 +1,341 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/functional>
#include <cuda/memory_resource>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/functional>
#include <cuda/std/type_traits>
#include <cuda/experimental/__multi_gpu/algorithm/reduce/reduce.h>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
namespace
{
struct custom_plus
{
template <class T>
[[nodiscard]] _CCCL_HOST_DEVICE_API constexpr T operator()(const T& lhs, const T& rhs) const
{
return lhs + rhs;
}
};
using custom_value = c2h::custom_type_t<c2h::accumulateable_t, c2h::less_comparable_t, c2h::equal_comparable_t>;
using value_types = c2h::type_list<cuda::std::int32_t, float, custom_value>;
using operators = c2h::type_list<::cuda::std::plus<>, ::cuda::maximum<>, custom_plus>;
static_assert(cudax::nccl_transportable<custom_value>);
template <typename T>
T make_value(int i)
{
return static_cast<T>(i);
}
template <>
custom_value make_value<>(int i)
{
custom_value ret{};
ret.key = static_cast<std::size_t>(i);
ret.val = static_cast<std::size_t>(i);
return ret;
};
template <class T, class Op>
[[nodiscard]] T get_identity()
{
if constexpr (cuda::std::is_same_v<Op, cuda::std::plus<>> || cuda::std::is_same_v<Op, custom_plus>)
{
return make_value<T>(0);
}
else if constexpr (cuda::std::is_same_v<Op, cuda::maximum<>>)
{
return cuda::std::numeric_limits<T>::lowest();
}
else
{
static_assert(cuda::std::__always_false_v<T, Op>, "Add handling");
}
}
// Run the full reduction, wait for it to finish, and check that `reduce` left its argument ranges
// untouched. This boilerplate is identical for every test regardless of how the inputs are shaped.
template <class Env, class T, class Op>
void do_reduce(cuda::std::span<cudax::nccl_communicator_ref> comms,
const std::vector<Env>& envs,
std::vector<cuda::device_buffer<T>>& in,
std::vector<typename cuda::device_buffer<T>::iterator>& outputs,
const T& init,
const T& ident,
Op op)
{
const auto envs_size = envs.size();
const auto in_copy = in;
const auto outputs_copy = outputs;
INFO("init = " << init);
INFO("ident = " << ident);
cudax::reduce(cudax::broadcasted, comms, envs, in, outputs, init, op, ident);
// cuda::std::execution::env has no operator==, so we can only compare the sizes.
REQUIRE(envs.size() == envs_size);
// Reduction call should not modify the inputs in any ways
REQUIRE(in.size() == in_copy.size());
for (cuda::std::size_t i = 0; i < in.size(); ++i)
{
INFO("device = " << i);
REQUIRE_THAT(in[i], Equals(in_copy[i]));
}
REQUIRE_THAT(outputs, Catch::Matchers::Equals(outputs_copy));
}
} // namespace
MULTI_GPU_TEST("reduce, one element per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
// Seed each reduction with a few hardcoded initializers. The init participates in the fold the
// same way on host and device, so any value works for every operator under test.
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Global rank `comms[i].rank()` contributes the single value `rank`. Each local rank also gets a
// one-element output buffer and an environment carrying its stream, so the reduction is
// stream-ordered on the correct device. `reference` mirrors the contributions of every global
// rank so we can fold them on the host exactly like `reduce` does on the device.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto values = {make_value<T>(comms[i].rank())};
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(
cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), 1, cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_reduce(comms, envs, in, outputs, init, ident, Op{});
const T expected = [&] {
std::vector<T> reference;
reference.reserve(comms.front().size());
for (int r = 0; r < comms.front().size(); ++r)
{
reference.push_back(make_value<T>(r));
}
return std::accumulate(reference.begin(), reference.end(), init, Op{});
}();
for (const auto& buf : out)
{
const auto exp = cuda::make_buffer(buf.stream(), cuda::mr::legacy_pinned_memory_resource{}, 1, expected);
REQUIRE_THAT(buf, Equals(exp));
}
}
MULTI_GPU_TEST("reduce, multiple elements per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
// Seed each reduction with a few hardcoded initializers. The init participates in the fold the
// same way on host and device, so any value works for every operator under test.
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Global rank `comms[i].rank()` contributes ten copies of `rank`. `reduce` first does a local
// CUB reduction of each rank's range, then combines the partials across ranks. Each local rank
// also gets a one-element output buffer and an environment carrying its stream. `reference`
// mirrors every global rank's ten contributions for the host-side fold.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto v = make_value<T>(comms[i].rank());
const std::vector<T> values(values_per_rank, v);
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(
cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), 1, cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_reduce(comms, envs, in, outputs, init, ident, Op{});
const T expected = [&] {
std::vector<T> reference;
reference.reserve(comms.front().size() * values_per_rank);
for (int r = 0; r < comms.front().size(); ++r)
{
const auto v = make_value<T>(r);
reference.insert(reference.end(), values_per_rank, v);
}
return std::accumulate(reference.begin(), reference.end(), init, Op{});
}();
for (const auto& buf : out)
{
const auto exp = cuda::make_buffer(buf.stream(), cuda::mr::legacy_pinned_memory_resource{}, 1, expected);
REQUIRE_THAT(buf, Equals(exp));
}
}
MULTI_GPU_TEST("reduce, some ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Even global ranks contribute ten copies of `rank`; odd global ranks contribute an empty input
// range. Rank 0 (the reduction root) is always non-empty. `reduce` must treat an empty rank as
// contributing nothing, exactly like `std::accumulate` over the surviving elements. `reference`
// mirrors that for the host-side fold.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto rank = comms[i].rank();
if (rank % 2 == 0)
{
const std::vector<T> values(values_per_rank, make_value<T>(rank));
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
}
else
{
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device()));
}
out.emplace_back(
cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), 1, cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_reduce(comms, envs, in, outputs, init, ident, Op{});
const T expected = [&] {
std::vector<T> reference;
reference.reserve(comms.front().size() * values_per_rank);
for (int r = 0; r < comms.front().size(); ++r)
{
if (r % 2 == 0)
{
reference.insert(reference.end(), values_per_rank, make_value<T>(r));
}
}
return std::accumulate(reference.begin(), reference.end(), init, Op{});
}();
for (const auto& buf : out)
{
const auto exp = cuda::make_buffer(buf.stream(), cuda::mr::legacy_pinned_memory_resource{}, 1, expected);
REQUIRE_THAT(buf, Equals(exp));
}
}
MULTI_GPU_TEST("reduce, all ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const auto init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// No rank contributes any element. Reducing nothing seeded by `init` is just `init`, so every
// output must equal `init` regardless of the operator.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device()));
out.emplace_back(
cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), 1, cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_reduce(comms, envs, in, outputs, init, ident, Op{});
// Reducing nothing seeded by `init` yields `init`, exactly like `std::accumulate` over an empty
// range.
const T expected = init;
for (const auto& buf : out)
{
const auto exp = cuda::make_buffer(buf.stream(), cuda::mr::legacy_pinned_memory_resource{}, 1, expected);
REQUIRE_THAT(buf, Equals(exp));
}
}

View File

@@ -0,0 +1,112 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/memory_resource>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/type_traits>
#include <cuda/experimental/__multi_gpu/algorithm/reduce/reduce.h>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
MULTI_GPU_TEST("reduce, range overloads default values", )
{
using T = cuda::std::int32_t;
using Op = ::cuda::std::plus<>;
constexpr auto init = T{};
constexpr T ident = cuda::identity_element<Op, T>();
constexpr auto op = Op{};
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const std::vector<T> values(values_per_rank, static_cast<T>(comms[i].rank()));
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(
cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), 1, cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
const auto expected = [&] {
std::vector<T> reference;
reference.reserve(comms.front().size() * values_per_rank);
for (int r = 0; r < comms.front().size(); ++r)
{
reference.insert(reference.end(), values_per_rank, static_cast<T>(r));
}
const auto val = std::accumulate(reference.begin(), reference.end(), init, op);
return cuda::make_buffer(cuda::stream_ref{::CUstream{}}, cuda::mr::legacy_pinned_memory_resource{}, 1, val);
}();
SECTION("Default init, op, ident (all)")
{
cudax::reduce(cudax::broadcasted, comms, envs, in, outputs);
for (const auto& buf : out)
{
REQUIRE_THAT(buf, Equals(expected));
}
}
SECTION("Default op, ident")
{
cudax::reduce(cudax::broadcasted, comms, envs, in, outputs, init);
for (const auto& buf : out)
{
REQUIRE_THAT(buf, Equals(expected));
}
}
SECTION("Default ident")
{
cudax::reduce(cudax::broadcasted, comms, envs, in, outputs, init, op);
for (const auto& buf : out)
{
REQUIRE_THAT(buf, Equals(expected));
}
}
SECTION("Default none")
{
cudax::reduce(cudax::broadcasted, comms, envs, in, outputs, init, op, ident);
for (const auto& buf : out)
{
REQUIRE_THAT(buf, Equals(expected));
}
}
}

View File

@@ -0,0 +1,347 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/functional>
#include <cuda/memory_resource>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/functional>
#include <cuda/std/type_traits>
#include <cuda/experimental/__multi_gpu/algorithm/reduce/reduce.h>
#include <exception>
#include <future>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
namespace
{
struct custom_plus
{
template <class T>
[[nodiscard]] _CCCL_HOST_DEVICE_API constexpr T operator()(const T& lhs, const T& rhs) const
{
return lhs + rhs;
}
};
using custom_value = c2h::custom_type_t<c2h::accumulateable_t, c2h::less_comparable_t, c2h::equal_comparable_t>;
using value_types = c2h::type_list<cuda::std::int32_t, float, custom_value>;
using operators = c2h::type_list<::cuda::std::plus<>, ::cuda::maximum<>, custom_plus>;
static_assert(cudax::nccl_transportable<custom_value>);
template <typename T>
T make_value(int i)
{
return static_cast<T>(i);
}
template <>
custom_value make_value<>(int i)
{
custom_value ret{};
ret.key = static_cast<std::size_t>(i);
ret.val = static_cast<std::size_t>(i);
return ret;
};
template <class T, class Op>
[[nodiscard]] T get_identity()
{
if constexpr (cuda::std::is_same_v<Op, cuda::std::plus<>> || cuda::std::is_same_v<Op, custom_plus>)
{
return make_value<T>(0);
}
else if constexpr (cuda::std::is_same_v<Op, cuda::maximum<>>)
{
return cuda::std::numeric_limits<T>::lowest();
}
else
{
static_assert(cuda::std::__always_false_v<T, Op>, "Add handling");
}
}
// Drive the reduction through the single-communicator overload of `reduce`, one thread per
// rank. That overload opens its own NCCL group on a single communicator, so issuing the
// per-rank calls serially on one thread would deadlock at `ncclGroupEnd`. Running each rank on
// its own thread lets the per-thread groups rendezvous across ranks. Only the `reduce` call
// happens on the worker threads; every Catch2 assertion runs on the main thread after the
// join, since the assertion macros are not safe to fire concurrently.
template <class Env, class T, class Op>
void do_reduce_threaded(
cuda::std::span<cudax::nccl_communicator_ref> comms,
std::vector<Env>& envs,
std::vector<cuda::device_buffer<T>>& in,
std::vector<typename cuda::device_buffer<T>::iterator>& outputs,
const T& init,
const T& ident,
Op op)
{
const auto in_copy = in;
const auto outputs_copy = outputs;
INFO("init = " << init);
INFO("ident = " << ident);
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::reduce(cudax::broadcasted, comms[i], envs[i], in[i], outputs[i], init, op, ident);
});
// Reduction call should not modify the inputs in any ways
REQUIRE(in.size() == in_copy.size());
for (cuda::std::size_t i = 0; i < in.size(); ++i)
{
INFO("device = " << i);
REQUIRE_THAT(in[i], Equals(in_copy[i]));
}
REQUIRE_THAT(outputs, Catch::Matchers::Equals(outputs_copy));
}
} // namespace
MULTI_GPU_TEST("reduce single-comm, one element per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
// Seed each reduction with a few hardcoded initializers. The init participates in the fold the
// same way on host and device, so any value works for every operator under test.
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Global rank `comms[i].rank()` contributes the single value `rank`. Each local rank also gets a
// one-element output buffer and an environment carrying its stream, so the reduction is
// stream-ordered on the correct device. `reference` mirrors the contributions of every global
// rank so we can fold them on the host exactly like `reduce` does on the device.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto values = {make_value<T>(comms[i].rank())};
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(
cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), 1, cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_reduce_threaded(comms, envs, in, outputs, init, ident, Op{});
const T expected = [&] {
std::vector<T> reference;
reference.reserve(comms.front().size());
for (int r = 0; r < comms.front().size(); ++r)
{
reference.push_back(make_value<T>(r));
}
return std::accumulate(reference.begin(), reference.end(), init, Op{});
}();
for (const auto& buf : out)
{
const auto exp = cuda::make_buffer(buf.stream(), cuda::mr::legacy_pinned_memory_resource{}, 1, expected);
REQUIRE_THAT(buf, Equals(exp));
}
}
MULTI_GPU_TEST("reduce single-comm, multiple elements per rank", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
// Seed each reduction with a few hardcoded initializers. The init participates in the fold the
// same way on host and device, so any value works for every operator under test.
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Global rank `comms[i].rank()` contributes ten copies of `rank`. `reduce` first does a local
// CUB reduction of each rank's range, then combines the partials across ranks. Each local rank
// also gets a one-element output buffer and an environment carrying its stream. `reference`
// mirrors every global rank's ten contributions for the host-side fold.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto v = make_value<T>(comms[i].rank());
const std::vector<T> values(values_per_rank, v);
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(
cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), 1, cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_reduce_threaded(comms, envs, in, outputs, init, ident, Op{});
const T expected = [&] {
std::vector<T> reference;
reference.reserve(comms.front().size() * values_per_rank);
for (int r = 0; r < comms.front().size(); ++r)
{
const auto v = make_value<T>(r);
reference.insert(reference.end(), values_per_rank, v);
}
return std::accumulate(reference.begin(), reference.end(), init, Op{});
}();
for (const auto& buf : out)
{
const auto exp = cuda::make_buffer(buf.stream(), cuda::mr::legacy_pinned_memory_resource{}, 1, expected);
REQUIRE_THAT(buf, Equals(exp));
}
}
MULTI_GPU_TEST("reduce single-comm, some ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const T init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// Even global ranks contribute ten copies of `rank`; odd global ranks contribute an empty input
// range. Rank 0 (the reduction root) is always non-empty. `reduce` must treat an empty rank as
// contributing nothing, exactly like `std::accumulate` over the surviving elements. `reference`
// mirrors that for the host-side fold.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const auto rank = comms[i].rank();
if (rank % 2 == 0)
{
const std::vector<T> values(values_per_rank, make_value<T>(rank));
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
}
else
{
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device()));
}
out.emplace_back(
cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), 1, cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_reduce_threaded(comms, envs, in, outputs, init, ident, Op{});
const T expected = [&] {
std::vector<T> reference;
reference.reserve(comms.front().size() * values_per_rank);
for (int r = 0; r < comms.front().size(); ++r)
{
if (r % 2 == 0)
{
reference.insert(reference.end(), values_per_rank, make_value<T>(r));
}
}
return std::accumulate(reference.begin(), reference.end(), init, Op{});
}();
for (const auto& buf : out)
{
const auto exp = cuda::make_buffer(buf.stream(), cuda::mr::legacy_pinned_memory_resource{}, 1, expected);
REQUIRE_THAT(buf, Equals(exp));
}
}
MULTI_GPU_TEST("reduce single-comm, all ranks empty", value_types, operators)
{
using T = c2h::get<0, TestType>;
using Op = c2h::get<1, TestType>;
const auto init = make_value<T>(GENERATE(0, 1, -1, 5));
const auto ident = get_identity<T, Op>();
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
// No rank contributes any element. Reducing nothing seeded by `init` is just `init`, so every
// output must equal `init` regardless of the operator.
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device()));
out.emplace_back(
cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), 1, cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
do_reduce_threaded(comms, envs, in, outputs, init, ident, Op{});
// Reducing nothing seeded by `init` yields `init`, exactly like `std::accumulate` over an empty
// range.
const T expected = init;
for (const auto& buf : out)
{
const auto exp = cuda::make_buffer(buf.stream(), cuda::mr::legacy_pinned_memory_resource{}, 1, expected);
REQUIRE_THAT(buf, Equals(exp));
}
}

View File

@@ -0,0 +1,122 @@
//===----------------------------------------------------------------------===//
//
// 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) 2026 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#include <cuda/buffer>
#include <cuda/memory_resource>
#include <cuda/std/cstdint>
#include <cuda/std/execution>
#include <cuda/std/type_traits>
#include <cuda/experimental/__multi_gpu/algorithm/reduce/reduce.h>
#include <exception>
#include <future>
#include <numeric>
#include <vector>
#include <algorithm_common.h>
#include <nccl_test_common.h>
#include <testing.cuh>
MULTI_GPU_TEST("reduce single-comm, overloads default values", )
{
using T = cuda::std::int32_t;
using Op = ::cuda::std::plus<>;
constexpr auto init = T{};
constexpr T ident = cuda::identity_element<Op, T>();
constexpr auto op = Op{};
auto comms = this->communicators();
auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<T>> in;
std::vector<cuda::device_buffer<T>> out;
std::vector<decltype(::cuda::std::execution::env{::cuda::stream_ref{streams[0]}})> envs;
in.reserve(comms.size());
out.reserve(comms.size());
envs.reserve(comms.size());
constexpr auto values_per_rank = 10;
for (cuda::std::size_t i = 0; i < comms.size(); ++i)
{
const std::vector<T> values(values_per_rank, static_cast<T>(comms[i].rank()));
in.emplace_back(cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), values));
out.emplace_back(
cuda::make_device_buffer<T>(streams[i], comms[i].logical_device().underlying_device(), 1, cuda::no_init));
envs.emplace_back(::cuda::std::execution::env{::cuda::stream_ref{streams[i]}});
}
auto outputs = make_output_iterators(out);
const auto expected = [&] {
std::vector<T> reference;
reference.reserve(comms.front().size() * values_per_rank);
for (int r = 0; r < comms.front().size(); ++r)
{
reference.insert(reference.end(), values_per_rank, static_cast<T>(r));
}
const auto val = std::accumulate(reference.begin(), reference.end(), init, op);
return cuda::make_buffer(cuda::stream_ref{::CUstream{}}, cuda::mr::legacy_pinned_memory_resource{}, 1, val);
}();
SECTION("Default init, op, ident (all)")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::reduce(cudax::broadcasted, comms[i], envs[i], in[i], outputs[i]);
});
for (const auto& buf : out)
{
REQUIRE_THAT(buf, Equals(expected));
}
}
SECTION("Default op, ident")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::reduce(cudax::broadcasted, comms[i], envs[i], in[i], outputs[i], init);
});
for (const auto& buf : out)
{
REQUIRE_THAT(buf, Equals(expected));
}
}
SECTION("Default ident")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::reduce(cudax::broadcasted, comms[i], envs[i], in[i], outputs[i], init, op);
});
for (const auto& buf : out)
{
REQUIRE_THAT(buf, Equals(expected));
}
}
SECTION("Default none")
{
run_threaded(comms.size(), [&](cuda::std::size_t i) {
cudax::reduce(cudax::broadcasted, comms[i], envs[i], in[i], outputs[i], init, op, ident);
});
for (const auto& buf : out)
{
REQUIRE_THAT(buf, Equals(expected));
}
}
}