[CCCL] 瘦身 + 补全: 移除 cudax/python/libcudacxx-tests 冗余文件, 新增 c2h 测试助手 + cmake 构建系统 + 8 个 CUDA thrust examples

变更摘要:
- 删除: cudax/ (783 files, 7.2M) — 实验性组件,竞赛不需要
- 删除: python/ (226 files, 2.0M) — Python 绑定,竞赛不需要
- 删除: libcudacxx/{test,benchmarks,codegen,cmake,share} (4432 files, 31M)
  保留: libcudacxx/include/ (1463 headers, cuda::std 编译依赖)
- 新增: c2h/ (27 files) — CUB Catch2 测试辅助头文件,编译 243 个测试必需
- 新增: cmake/ (29 files) — CCCL 原生 CMake 构建系统
- 新增: thrust/examples/cuda/ (7 files) + cpp_integration/ (1 file)
  async_reduce, custom_temporary_allocation, explicit_cuda_stream,
  global_device_vector, range_view, unwrap_pointer, wrap_pointer, device

结果: cccl_upstream 从 74M→35M (瘦身 53%), 核心内容 100% 保留:
  27/27 tuning headers, 78 benchmarks, 243 tests,
  60 thrust examples, 18 CUB examples, 全部编译头文件
This commit is contained in:
muh-bot
2026-08-03 12:39:26 +00:00
parent a2a5dd8f00
commit 24ef6a91b5
5439 changed files with 0 additions and 719516 deletions

View File

@@ -1,73 +0,0 @@
#===----------------------------------------------------------------------===##
#
# 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.
#
#===----------------------------------------------------------------------===##
cccl_get_nccl(QUIET)
option(
cudax_ENABLE_NCCL
"Enable cudax tests that link against NCCL."
"${NCCL_FOUND}"
)
# TODO(jfaibussowit):
#
# Remove this once NCCL supports windows and we have added NCCL to the NVHPC
# devcontainers.
macro(hack_cudax_handle_missing_nccl)
if (NOT cudax_ENABLE_NCCL)
message(
AUTHOR_WARNING
"Function should only be called if we have requested NCCL, this is a developer error"
)
endif()
option(
HACK_cudax_ALLOW_MISSING_NCCL
"Hack to work around CI issues since NCCL does not (yet) officially support Windows"
TRUE
)
if (NCCL_FOUND)
# If we have somehow already found NCCL (e.g. they have shipped support but we have
# forgotten to remove this function), then we have nothing to do. We still call
# cccl_get_nccl() though because we want the found version to appear in the logs. The
# previous call was with QUIET which won't do that.
cccl_get_nccl(REQUIRED)
elseif (HACK_cudax_ALLOW_MISSING_NCCL)
set(cudax_ENABLE_NCCL FALSE)
set(cudax_ENABLE_NCCL FALSE CACHE BOOL "" FORCE)
else()
# This will cause a hard error
cccl_get_nccl(REQUIRED)
endif()
endmacro()
if (cudax_ENABLE_NCCL)
if (WIN32 OR ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "NVHPC"))
hack_cudax_handle_missing_nccl()
else()
cccl_get_nccl(REQUIRED)
endif()
endif()
function(cudax_add_multi_gpu_test sub_prefix target_name_var source)
cmake_path(GET source STEM filename)
cudax_add_catch2_test("${target_name_var}" "multi_gpu.${sub_prefix}.${filename}" "${source}" ${ARGN})
target_include_directories(
"${${target_name_var}}"
PRIVATE "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/include"
)
set("${target_name_var}" "${${target_name_var}}" PARENT_SCOPE)
endfunction()
add_subdirectory(algorithms)
add_subdirectory(communicators)
add_subdirectory(concepts)
add_subdirectory(nccl)

View File

@@ -1,13 +0,0 @@
#===----------------------------------------------------------------------===##
#
# 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

@@ -1,20 +0,0 @@
#===----------------------------------------------------------------------===##
#
# 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

@@ -1,388 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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

@@ -1,112 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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

@@ -1,289 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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

@@ -1,120 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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

@@ -1,20 +0,0 @@
#===----------------------------------------------------------------------===##
#
# 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

@@ -1,389 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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

@@ -1,112 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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

@@ -1,290 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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

@@ -1,120 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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

@@ -1,20 +0,0 @@
#===----------------------------------------------------------------------===##
#
# 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

@@ -1,341 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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

@@ -1,112 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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

@@ -1,347 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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

@@ -1,122 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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));
}
}
}

View File

@@ -1,11 +0,0 @@
#===----------------------------------------------------------------------===##
#
# 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(nccl)

View File

@@ -1,20 +0,0 @@
#===----------------------------------------------------------------------===##
#
# 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("communicators.nccl" test_target "${src}")
target_link_libraries(${test_target} PRIVATE NCCL::nccl)
endforeach()

View File

@@ -1,223 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/devices>
#include <cuda/std/type_traits>
#include <cuda/std/utility>
#include <cuda/experimental/__multi_gpu/nccl_communicator.h>
#include <cuda/experimental/__multi_gpu/nccl_communicator_ref.h>
#include <nccl.h>
#include <nccl_test_common.h>
namespace
{
[[nodiscard]] ncclComm_t make_nccl_communicator_handle()
{
if (cuda::devices.size() == 0)
{
SKIP("No CUDA devices visible");
}
const int device = cuda::devices[0].get();
ncclComm_t handle{};
const ncclResult_t result = ncclCommInitAll(&handle, 1, &device);
INFO("NCCL: " << ncclGetErrorString(result));
REQUIRE(result == ncclSuccess);
return handle;
}
} // namespace
C2H_TEST("nccl_communicator_ref typedefs", "[multi_gpu]")
{
STATIC_REQUIRE(::cuda::std::is_same_v<cudax::nccl_communicator_ref::native_handle_type, ncclComm_t>);
STATIC_REQUIRE(
::cuda::std::is_same_v<cudax::nccl_communicator_ref::group_guard_type,
decltype(::cuda::std::declval<const cudax::nccl_communicator_ref&>().group_guard())>);
}
C2H_TEST("nccl_communicator(s) not constructible from NCCL_COMM_NULL", "[multi_gpu]")
{
SECTION("ref")
{
STATIC_REQUIRE(!::cuda::std::is_constructible_v<cudax::nccl_communicator_ref, decltype(NCCL_COMM_NULL)>);
STATIC_REQUIRE(!::cuda::std::is_constructible_v<cudax::nccl_communicator_ref, cuda::std::nullptr_t>);
}
SECTION("owning")
{
STATIC_REQUIRE(!::cuda::std::is_constructible_v<cudax::nccl_communicator, decltype(NCCL_COMM_NULL)>);
STATIC_REQUIRE(!::cuda::std::is_constructible_v<cudax::nccl_communicator, cuda::std::nullptr_t>);
STATIC_REQUIRE(!::cuda::std::is_constructible_v<cudax::nccl_communicator, ncclComm_t>);
}
}
C2H_TEST("nccl_communicator basic", "[multi_gpu][nccl]")
{
SECTION("is move-only")
{
STATIC_REQUIRE(!cuda::std::is_copy_constructible_v<cudax::nccl_communicator>);
STATIC_REQUIRE(!cuda::std::is_copy_assignable_v<cudax::nccl_communicator>);
STATIC_REQUIRE(cuda::std::is_move_constructible_v<cudax::nccl_communicator>);
STATIC_REQUIRE(cuda::std::is_nothrow_move_constructible_v<cudax::nccl_communicator>);
STATIC_REQUIRE(cuda::std::is_move_assignable_v<cudax::nccl_communicator>);
STATIC_REQUIRE(cuda::std::is_nothrow_move_assignable_v<cudax::nccl_communicator>);
}
SECTION("factory construction")
{
STATIC_REQUIRE(
cuda::std::is_same_v<decltype(cudax::nccl_communicator::from_native_handle(cuda::std::declval<ncclComm_t>())),
cudax::nccl_communicator>);
//! [nccl_communicator_construction]
const ncclComm_t handle = make_nccl_communicator_handle();
auto comm = cuda::experimental::nccl_communicator::from_native_handle(handle);
// comm owns the handle now
REQUIRE(comm.native_handle() == handle);
//! [nccl_communicator_construction]
}
SECTION("factory construction with logical device")
{
STATIC_REQUIRE(
cuda::std::is_same_v<decltype(cudax::nccl_communicator::from_native_handle(
cuda::std::declval<ncclComm_t>(), cuda::std::declval<cudax::logical_device>())),
cudax::nccl_communicator>);
//! [nccl_communicator_construction_with_logical_device]
const ncclComm_t handle = make_nccl_communicator_handle();
const auto device = cudax::logical_device{cuda::devices[0]};
auto comm = cudax::nccl_communicator::from_native_handle(handle, device);
REQUIRE(comm.native_handle() == handle);
REQUIRE(comm.logical_device() == device);
//! [nccl_communicator_construction_with_logical_device]
}
SECTION("no_init construction")
{
STATIC_REQUIRE(cuda::std::is_nothrow_constructible_v<cudax::nccl_communicator, cuda::no_init_t>);
//! [nccl_communicator_no_init_construction]
const auto comm = cudax::nccl_communicator{cuda::no_init};
REQUIRE(comm.native_handle() == ncclComm_t{NCCL_COMM_NULL});
//! [nccl_communicator_no_init_construction]
REQUIRE(comm.rank() == 0);
REQUIRE(comm.size() == 0);
}
SECTION("release")
{
//! [nccl_communicator_release]
const ncclComm_t handle = make_nccl_communicator_handle();
auto comm = cuda::experimental::nccl_communicator::from_native_handle(handle);
const auto released_handle = comm.release();
// comm contains the null handle after release
REQUIRE(comm.native_handle() == ncclComm_t{NCCL_COMM_NULL});
REQUIRE(released_handle == handle);
//! [nccl_communicator_release]
// so that we clean up properly
[[maybe_unused]] const auto _ = cudax::nccl_communicator::from_native_handle(handle);
}
SECTION("move construction")
{
//! [nccl_communicator_move_construction]
const ncclComm_t handle = make_nccl_communicator_handle();
auto source = cudax::nccl_communicator::from_native_handle(handle);
auto destination = cudax::nccl_communicator{cuda::std::move(source)};
// moved-from communicator is now invalid
REQUIRE(source.native_handle() == ncclComm_t{NCCL_COMM_NULL});
REQUIRE(destination.native_handle() == handle);
//! [nccl_communicator_move_construction]
}
SECTION("move assignment")
{
//! [nccl_communicator_move_assignment]
auto source = cuda::experimental::nccl_communicator::from_native_handle(make_nccl_communicator_handle());
auto destination = cuda::experimental::nccl_communicator::from_native_handle(make_nccl_communicator_handle());
// Save the native handle to verify that ownership is transferred.
const auto handle = source.native_handle();
destination = cuda::std::move(source);
REQUIRE(source.native_handle() == ncclComm_t{NCCL_COMM_NULL});
REQUIRE(destination.native_handle() == handle);
//! [nccl_communicator_move_assignment]
}
}
MULTI_GPU_TEST("nccl_communicator_ref basic", )
{
SECTION("rank and size")
{
int i = 0;
for (auto& comm : this->communicators())
{
REQUIRE(comm.rank() == i);
REQUIRE(comm.size() == static_cast<int>(cuda::devices.size()));
++i;
}
}
SECTION("native handle")
{
for (auto& comm : this->communicators())
{
REQUIRE(comm.native_handle() != NCCL_COMM_NULL);
}
}
SECTION("logical device")
{
int i = 0;
for (auto& comm : this->communicators())
{
REQUIRE(comm.logical_device().underlying_device() == cuda::devices[i]);
++i;
}
}
SECTION("group_guard round trip")
{
// Opening and closing a guard with no enqueued ops must not throw.
[[maybe_unused]] auto g = this->communicators().front().group_guard();
}
SECTION("device mismatch throws")
{
if (cuda::devices.size() > 1)
{
REQUIRE_THROWS_WITH(
cudax::nccl_communicator_ref(this->communicators()[0].native_handle(), cudax::logical_device{cuda::devices[1]}),
"Inconsistent devices, NCCL communicator device and provided logical device do not match");
}
}
}

View File

@@ -1,487 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/devices>
#include <cuda/functional>
#include <cuda/memory_pool>
#include <cuda/memory_resource>
#include <cuda/std/cstddef>
#include <cuda/std/cstdint>
#include <cuda/std/functional>
#include <vector>
#include <nccl_test_common.h>
namespace
{
constexpr cuda::std::int32_t ROOT_RANK = 0;
} // namespace
MULTI_GPU_TEST("nccl_communicator_ref all_reduce sum", )
{
auto streams = nccl_test_util::make_streams();
// Rank r contributes {r+1, r+1, r+1, r+1}; the element-wise sum is the n-th triangular number.
std::vector<cuda::device_buffer<cuda::std::int32_t>> send;
std::vector<cuda::device_buffer<cuda::std::int32_t>> recv;
for (int i = 0; i < static_cast<int>(cuda::devices.size()); ++i)
{
auto pool = cuda::device_default_memory_pool(cuda::devices[i]);
const auto values = {i + 1, i + 1, i + 1, i + 1};
auto& s = send.emplace_back(streams[i], pool, values);
recv.emplace_back(cuda::make_buffer(streams[i], pool, s.size(), cuda::std::int32_t{-1}));
}
{
auto g = this->communicators().front().group_guard();
for (cuda::std::size_t i = 0; i < cuda::devices.size(); ++i)
{
this->communicators()[i].all_reduce(
g, send[i].data(), recv[i].data(), send[i].size(), ::cuda::std::plus<>{}, streams[i]);
}
}
for (auto& stream : streams)
{
stream.sync();
}
const auto sum = static_cast<cuda::std::int32_t>(cuda::devices.size() * (cuda::devices.size() + 1) / 2);
for (auto& buf : recv)
{
auto pool = cuda::mr::legacy_pinned_memory_resource{};
const auto expected = cuda::make_buffer(buf.stream(), pool, buf.size(), sum);
const auto actual = cuda::make_buffer(buf.stream(), pool, buf);
REQUIRE_THAT(actual, Equals(expected));
}
}
MULTI_GPU_TEST("nccl_communicator_ref all_reduce maximum", )
{
auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<cuda::std::int32_t>> send;
std::vector<cuda::device_buffer<cuda::std::int32_t>> recv;
for (int i = 0; i < static_cast<int>(cuda::devices.size()); ++i)
{
auto pool = cuda::device_default_memory_pool(cuda::devices[i]);
const auto values = {i, 100 - i, 2 * i};
auto& s = send.emplace_back(streams[i], pool, values);
recv.emplace_back(cuda::make_buffer(streams[i], pool, s.size(), cuda::std::int32_t{-1}));
}
{
auto g = this->communicators().front().group_guard();
for (cuda::std::size_t i = 0; i < cuda::devices.size(); ++i)
{
this->communicators()[i].all_reduce(
g, send[i].data(), recv[i].data(), send[i].size(), ::cuda::maximum<>{}, streams[i]);
}
}
for (auto& stream : streams)
{
stream.sync();
}
const auto n = static_cast<cuda::std::int32_t>(cuda::devices.size());
for (auto& buf : recv)
{
auto pool = cuda::mr::legacy_pinned_memory_resource{};
const auto expected_values = {n - 1, cuda::std::int32_t{100}, 2 * (n - 1)};
const auto expected = cuda::make_buffer(buf.stream(), pool, expected_values);
const auto actual = cuda::make_buffer(buf.stream(), pool, buf);
REQUIRE_THAT(actual, Equals(expected));
}
}
MULTI_GPU_TEST("nccl_communicator_ref reduce sum to root 0", )
{
auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<cuda::std::int32_t>> send;
std::vector<cuda::device_buffer<cuda::std::int32_t>> recv;
for (int i = 0; i < static_cast<int>(cuda::devices.size()); ++i)
{
auto pool = cuda::device_default_memory_pool(cuda::devices[i]);
const auto values = {i + 1, i + 1, i + 1, i + 1};
auto& s = send.emplace_back(streams[i], pool, values);
recv.emplace_back(cuda::make_buffer(streams[i], pool, s.size(), cuda::std::int32_t{-1}));
}
{
auto g = this->communicators().front().group_guard();
for (cuda::std::size_t i = 0; i < cuda::devices.size(); ++i)
{
this->communicators()[i].reduce(
g, send[i].data(), recv[i].data(), send[i].size(), ::cuda::std::plus<>{}, ROOT_RANK, streams[i]);
}
}
for (auto& stream : streams)
{
stream.sync();
}
const auto sum = static_cast<cuda::std::int32_t>(cuda::devices.size() * (cuda::devices.size() + 1) / 2);
auto pool = cuda::mr::legacy_pinned_memory_resource{};
const auto actual = cuda::make_buffer(recv[ROOT_RANK].stream(), pool, recv[ROOT_RANK]);
const auto expected = cuda::make_buffer(actual.stream(), pool, actual.size(), sum);
REQUIRE_THAT(actual, Equals(expected));
}
MULTI_GPU_TEST("nccl_communicator_ref broadcast from root 0", )
{
auto streams = nccl_test_util::make_streams();
// Only root's send buffer is read; give every rank the same literal so the source is obvious.
std::vector<cuda::device_buffer<cuda::std::int32_t>> send;
std::vector<cuda::device_buffer<cuda::std::int32_t>> recv;
for (int i = 0; i < static_cast<int>(cuda::devices.size()); ++i)
{
auto pool = cuda::device_default_memory_pool(cuda::devices[i]);
const auto values = {10, 20, 30, 40};
auto& s = send.emplace_back(streams[i], pool, values);
recv.emplace_back(cuda::make_buffer(streams[i], pool, s.size(), cuda::std::int32_t{-1}));
}
{
auto g = this->communicators().front().group_guard();
for (cuda::std::size_t i = 0; i < cuda::devices.size(); ++i)
{
this->communicators()[i].broadcast(g, send[i].data(), recv[i].data(), send[i].size(), ROOT_RANK, streams[i]);
}
}
for (auto& stream : streams)
{
stream.sync();
}
for (auto& buf : recv)
{
auto pool = cuda::mr::legacy_pinned_memory_resource{};
const auto expected_values = {
cuda::std::int32_t{10}, cuda::std::int32_t{20}, cuda::std::int32_t{30}, cuda::std::int32_t{40}};
const auto expected = cuda::make_buffer(buf.stream(), pool, expected_values);
const auto actual = cuda::make_buffer(buf.stream(), pool, buf);
REQUIRE_THAT(actual, Equals(expected));
}
}
MULTI_GPU_TEST("nccl_communicator_ref all_gather", )
{
auto streams = nccl_test_util::make_streams();
// Rank r contributes {10*r, 10*r+1}; every rank ends up with the concatenation in rank order.
std::vector<cuda::device_buffer<cuda::std::int32_t>> send;
std::vector<cuda::device_buffer<cuda::std::int32_t>> recv;
for (int i = 0; i < static_cast<int>(cuda::devices.size()); ++i)
{
auto pool = cuda::device_default_memory_pool(cuda::devices[i]);
const auto values = {10 * i, 10 * i + 1};
auto& s = send.emplace_back(streams[i], pool, values);
recv.emplace_back(cuda::make_buffer(streams[i], pool, s.size() * cuda::devices.size(), cuda::std::int32_t{-1}));
}
{
auto g = this->communicators().front().group_guard();
for (cuda::std::size_t i = 0; i < cuda::devices.size(); ++i)
{
this->communicators()[i].all_gather(g, send[i].data(), recv[i].data(), send[i].size(), streams[i]);
}
}
for (auto& stream : streams)
{
stream.sync();
}
const cuda::std::size_t per_rank = send.front().size();
std::vector<cuda::std::int32_t> expected_values(per_rank * cuda::devices.size());
for (cuda::std::size_t r = 0; r < cuda::devices.size(); ++r)
{
expected_values[r * per_rank] = static_cast<cuda::std::int32_t>(10 * r);
expected_values[(r * per_rank) + 1] = static_cast<cuda::std::int32_t>((10 * r) + 1);
}
for (auto& buf : recv)
{
auto pool = cuda::mr::legacy_pinned_memory_resource{};
const auto expected = cuda::make_buffer<cuda::std::int32_t>(buf.stream(), pool, expected_values);
const auto actual = cuda::make_buffer(buf.stream(), pool, buf);
REQUIRE_THAT(actual, Equals(expected));
}
}
MULTI_GPU_TEST("nccl_communicator_ref gather_v to root 0", )
{
auto streams = nccl_test_util::make_streams();
// Rank r contributes (2 + r) elements: {10*r, 10*r+1, ...}. Root concatenates them in rank order.
std::vector<cuda::device_buffer<cuda::std::int32_t>> send;
for (int i = 0; i < static_cast<int>(cuda::devices.size()); ++i)
{
auto pool = cuda::device_default_memory_pool(cuda::devices[i]);
std::vector<cuda::std::int32_t> h(2 + static_cast<cuda::std::size_t>(i));
for (cuda::std::size_t k = 0; k < h.size(); ++k)
{
h[k] = (10 * i) + static_cast<int>(k);
}
send.emplace_back(streams[i], pool, h);
}
std::vector<cuda::std::size_t> recv_counts(cuda::devices.size());
std::vector<cuda::std::size_t> displs(cuda::devices.size());
cuda::std::size_t total = 0;
for (cuda::std::size_t r = 0; r < cuda::devices.size(); ++r)
{
recv_counts[r] = send[r].size();
displs[r] = total;
total += recv_counts[r];
}
auto root_pool = cuda::device_default_memory_pool(cuda::devices[ROOT_RANK]);
auto recv = cuda::make_buffer(streams[ROOT_RANK], root_pool, total, cuda::std::int32_t{-1});
{
auto g = this->communicators().front().group_guard();
for (cuda::std::size_t i = 0; i < cuda::devices.size(); ++i)
{
this->communicators()[i].gather_v(
g, send[i].data(), send[i].size(), recv.data(), recv_counts.data(), displs.data(), ROOT_RANK, streams[i]);
}
}
for (auto& stream : streams)
{
stream.sync();
}
std::vector<cuda::std::int32_t> expected_values(total);
for (cuda::std::size_t r = 0; r < cuda::devices.size(); ++r)
{
for (cuda::std::size_t k = 0; k < recv_counts[r]; ++k)
{
expected_values[displs[r] + k] = static_cast<cuda::std::int32_t>((10 * r) + k);
}
}
auto pool = cuda::mr::legacy_pinned_memory_resource{};
const auto expected = cuda::make_buffer<cuda::std::int32_t>(recv.stream(), pool, expected_values);
const auto actual = cuda::make_buffer(recv.stream(), pool, recv);
REQUIRE_THAT(actual, Equals(expected));
}
MULTI_GPU_TEST("nccl_communicator_ref all_to_all_v", )
{
auto streams = nccl_test_util::make_streams();
// Two elements exchanged with each peer; the block for peer j sits at offset block*j.
constexpr cuda::std::size_t block = 2;
std::vector<cuda::std::size_t> counts(cuda::devices.size(), block);
std::vector<cuda::std::size_t> displs(cuda::devices.size());
for (cuda::std::size_t j = 0; j < cuda::devices.size(); ++j)
{
displs[j] = block * j;
}
// Rank r block destined for peer j encodes 100*r + 10*j + k.
std::vector<cuda::device_buffer<cuda::std::int32_t>> send;
std::vector<cuda::device_buffer<cuda::std::int32_t>> recv;
for (int i = 0; i < static_cast<int>(cuda::devices.size()); ++i)
{
auto pool = cuda::device_default_memory_pool(cuda::devices[i]);
std::vector<cuda::std::int32_t> h(block * cuda::devices.size());
for (cuda::std::size_t j = 0; j < cuda::devices.size(); ++j)
{
h[block * j] = static_cast<cuda::std::int32_t>((100 * i) + (10 * j));
h[(block * j) + 1] = static_cast<cuda::std::int32_t>((100 * i) + (10 * j) + 1);
}
auto& s = send.emplace_back(streams[i], pool, h);
recv.emplace_back(cuda::make_buffer(streams[i], pool, s.size(), cuda::std::int32_t{-1}));
}
{
auto g = this->communicators().front().group_guard();
for (cuda::std::size_t i = 0; i < cuda::devices.size(); ++i)
{
this->communicators()[i].all_to_all_v(
g, send[i].data(), counts.data(), displs.data(), recv[i].data(), counts.data(), displs.data(), streams[i]);
}
}
for (auto& stream : streams)
{
stream.sync();
}
// Rank r receives from peer i the block i sent to r, placed at block*i: 100*i + 10*r + k.
for (cuda::std::size_t r = 0; r < cuda::devices.size(); ++r)
{
std::vector<cuda::std::int32_t> expected_values(block * cuda::devices.size());
for (cuda::std::size_t i = 0; i < cuda::devices.size(); ++i)
{
expected_values[block * i] = static_cast<cuda::std::int32_t>((100 * i) + (10 * r));
expected_values[(block * i) + 1] = static_cast<cuda::std::int32_t>((100 * i) + (10 * r) + 1);
}
auto pool = cuda::mr::legacy_pinned_memory_resource{};
const auto expected = cuda::make_buffer<cuda::std::int32_t>(recv[r].stream(), pool, expected_values);
const auto actual = cuda::make_buffer(recv[r].stream(), pool, recv[r]);
REQUIRE_THAT(actual, Equals(expected));
}
}
#if NCCL_VERSION_CODE >= NCCL_VERSION(2, 28, 0)
MULTI_GPU_TEST("nccl_communicator_ref gather to root", )
{
auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<cuda::std::int32_t>> send;
std::vector<cuda::device_buffer<cuda::std::int32_t>> recv;
for (int i = 0; i < static_cast<int>(cuda::devices.size()); ++i)
{
auto pool = cuda::device_default_memory_pool(cuda::devices[i]);
const auto values = {10 * i, 10 * i + 1};
auto& s = send.emplace_back(streams[i], pool, values);
recv.emplace_back(cuda::make_buffer(streams[i], pool, s.size() * cuda::devices.size(), cuda::std::int32_t{-1}));
}
{
auto g = this->communicators().front().group_guard();
for (cuda::std::size_t i = 0; i < cuda::devices.size(); ++i)
{
this->communicators()[i].gather(g, send[i].data(), recv[i].data(), send[i].size(), ROOT_RANK, streams[i]);
}
}
for (auto& stream : streams)
{
stream.sync();
}
const cuda::std::size_t per_rank = send.front().size();
std::vector<cuda::std::int32_t> expected_values(per_rank * cuda::devices.size());
for (cuda::std::size_t r = 0; r < cuda::devices.size(); ++r)
{
expected_values[r * per_rank] = static_cast<cuda::std::int32_t>(10 * r);
expected_values[r * per_rank + 1] = static_cast<cuda::std::int32_t>(10 * r + 1);
}
auto pool = cuda::mr::legacy_pinned_memory_resource{};
const auto expected = cuda::make_buffer<cuda::std::int32_t>(recv[ROOT_RANK].stream(), pool, expected_values);
const auto actual = cuda::make_buffer(recv[ROOT_RANK].stream(), pool, recv[ROOT_RANK]);
REQUIRE_THAT(actual, Equals(expected));
}
MULTI_GPU_TEST("nccl_communicator_ref all_to_all", )
{
auto streams = nccl_test_util::make_streams();
// Two elements exchanged with each peer; the block for peer j sits at offset block*j.
constexpr cuda::std::size_t block = 2;
// Rank r block destined for peer j encodes 100*r + 10*j + k.
std::vector<cuda::device_buffer<cuda::std::int32_t>> send;
std::vector<cuda::device_buffer<cuda::std::int32_t>> recv;
for (int i = 0; i < static_cast<int>(cuda::devices.size()); ++i)
{
auto pool = cuda::device_default_memory_pool(cuda::devices[i]);
std::vector<cuda::std::int32_t> h(block * cuda::devices.size());
for (cuda::std::size_t j = 0; j < cuda::devices.size(); ++j)
{
h[block * j] = static_cast<cuda::std::int32_t>(100 * i + 10 * j);
h[block * j + 1] = static_cast<cuda::std::int32_t>(100 * i + 10 * j + 1);
}
auto& s = send.emplace_back(streams[i], pool, h);
recv.emplace_back(cuda::make_buffer(streams[i], pool, s.size(), cuda::std::int32_t{-1}));
}
{
auto g = this->communicators().front().group_guard();
for (cuda::std::size_t i = 0; i < cuda::devices.size(); ++i)
{
this->communicators()[i].all_to_all(g, send[i].data(), recv[i].data(), block, streams[i]);
}
}
for (auto& stream : streams)
{
stream.sync();
}
// Rank r receives from peer i the block i sent to r, placed at block*i: 100*i + 10*r + k.
for (cuda::std::size_t r = 0; r < cuda::devices.size(); ++r)
{
std::vector<cuda::std::int32_t> expected_values(block * cuda::devices.size());
for (cuda::std::size_t i = 0; i < cuda::devices.size(); ++i)
{
expected_values[block * i] = static_cast<cuda::std::int32_t>(100 * i + 10 * r);
expected_values[block * i + 1] = static_cast<cuda::std::int32_t>(100 * i + 10 * r + 1);
}
auto pool = cuda::mr::legacy_pinned_memory_resource{};
const auto expected = cuda::make_buffer<cuda::std::int32_t>(recv[r].stream(), pool, expected_values);
const auto actual = cuda::make_buffer(recv[r].stream(), pool, recv[r]);
REQUIRE_THAT(actual, Equals(expected));
}
}
#endif // NCCL_VERSION_CODE >= NCCL_VERSION(2, 28, 0)

View File

@@ -1,92 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/functional>
#include <cuda/std/functional>
#include <cuda/experimental/__multi_gpu/concepts.h>
#include <cuda/experimental/__multi_gpu/nccl_communicator_ref.h>
#include <functional>
#include <testing.cuh>
namespace
{
struct payload
{
int from;
int value;
};
struct non_trivial
{
non_trivial(const non_trivial&) {} // NOLINT(modernize-use-equals-default)
int value;
};
struct unsupported_op
{};
} // namespace
C2H_TEST("nccl_communicator_ref concept conformance", "[multi_gpu][nccl]")
{
STATIC_REQUIRE(cudax::__communicator<cudax::nccl_communicator_ref>);
STATIC_REQUIRE(cudax::__has_send<cudax::nccl_communicator_ref, int*>);
STATIC_REQUIRE(cudax::__has_send<cudax::nccl_communicator_ref, payload*>);
STATIC_REQUIRE(!cudax::__has_send<cudax::nccl_communicator_ref, non_trivial*>);
STATIC_REQUIRE(cudax::__has_recv<cudax::nccl_communicator_ref, int*>);
STATIC_REQUIRE(cudax::__has_recv<cudax::nccl_communicator_ref, payload*>);
STATIC_REQUIRE(!cudax::__has_recv<cudax::nccl_communicator_ref, non_trivial*>);
STATIC_REQUIRE(cudax::__has_reduce<cudax::nccl_communicator_ref, int*>);
STATIC_REQUIRE(!cudax::__has_reduce<cudax::nccl_communicator_ref, payload*>);
STATIC_REQUIRE(cudax::__has_all_reduce<cudax::nccl_communicator_ref>);
STATIC_REQUIRE(!cudax::__has_all_reduce<cudax::nccl_communicator_ref, payload*>);
STATIC_REQUIRE(cudax::__has_gather<cudax::nccl_communicator_ref>);
STATIC_REQUIRE(cudax::__has_gather<cudax::nccl_communicator_ref, payload*>);
STATIC_REQUIRE(!cudax::__has_gather<cudax::nccl_communicator_ref, non_trivial*>);
STATIC_REQUIRE(cudax::__has_gather_v<cudax::nccl_communicator_ref>);
STATIC_REQUIRE(cudax::__has_gather_v<cudax::nccl_communicator_ref, payload*>);
STATIC_REQUIRE(!cudax::__has_gather_v<cudax::nccl_communicator_ref, non_trivial*>);
STATIC_REQUIRE(cudax::__has_all_gather<cudax::nccl_communicator_ref>);
STATIC_REQUIRE(cudax::__has_all_gather<cudax::nccl_communicator_ref, payload*>);
STATIC_REQUIRE(!cudax::__has_all_gather<cudax::nccl_communicator_ref, non_trivial*>);
STATIC_REQUIRE(cudax::__has_broadcast<cudax::nccl_communicator_ref>);
STATIC_REQUIRE(cudax::__has_broadcast<cudax::nccl_communicator_ref, payload*>);
STATIC_REQUIRE(!cudax::__has_broadcast<cudax::nccl_communicator_ref, non_trivial*>);
STATIC_REQUIRE(cudax::__has_all_to_all<cudax::nccl_communicator_ref>);
STATIC_REQUIRE(cudax::__has_all_to_all<cudax::nccl_communicator_ref, payload*>);
STATIC_REQUIRE(!cudax::__has_all_to_all<cudax::nccl_communicator_ref, non_trivial*>);
STATIC_REQUIRE(cudax::__has_all_to_all_v<cudax::nccl_communicator_ref>);
STATIC_REQUIRE(cudax::__has_all_to_all_v<cudax::nccl_communicator_ref, payload*>);
STATIC_REQUIRE(!cudax::__has_all_to_all_v<cudax::nccl_communicator_ref, non_trivial*>);
STATIC_REQUIRE(cudax::nccl_transportable<int>);
STATIC_REQUIRE(cudax::nccl_transportable<int*>);
STATIC_REQUIRE(cudax::nccl_transportable<const int*>);
STATIC_REQUIRE(cudax::nccl_transportable<const volatile int* const>);
STATIC_REQUIRE(cudax::nccl_transportable<float>);
STATIC_REQUIRE(cudax::nccl_transportable<::cuda::std::int32_t>);
STATIC_REQUIRE(cudax::nccl_transportable<void>);
STATIC_REQUIRE(cudax::nccl_transportable<payload>);
STATIC_REQUIRE(!cudax::nccl_transportable<non_trivial>);
STATIC_REQUIRE(cudax::nccl_reducible<int, cuda::std::plus<>>);
STATIC_REQUIRE(cudax::nccl_reducible<int, cuda::std::multiplies<>>);
STATIC_REQUIRE(cudax::nccl_reducible<int, cuda::maximum<>>);
STATIC_REQUIRE(cudax::nccl_reducible<int, cuda::minimum<>>);
STATIC_REQUIRE(!cudax::nccl_reducible<payload, cuda::std::plus<>>);
STATIC_REQUIRE(!cudax::nccl_reducible<int, unsupported_op>);
STATIC_REQUIRE(cudax::nccl_reducible<int, std::plus<>>);
STATIC_REQUIRE(cudax::nccl_reducible<int, std::multiplies<>>);
STATIC_REQUIRE(!cudax::nccl_reducible<payload, std::plus<>>);
}

View File

@@ -1,150 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/devices>
#include <cuda/memory_pool>
#include <cuda/memory_resource>
#include <cuda/std/cstdint>
#include <vector>
#include <nccl_test_common.h>
namespace
{
struct payload
{
cuda::std::int32_t from;
cuda::std::int32_t index;
};
} // namespace
// Ring exchange via send/recv. Rank r contributes {r, r, r}.
MULTI_GPU_TEST("nccl_communicator_ref send/recv ring", )
{
if (cuda::devices.size() == 1)
{
// NCCL disallows self send/recv on a 1-rank comm.
REQUIRE(this->communicators().front().rank() == 0);
REQUIRE(this->communicators().front().size() == 1);
return;
}
const int n = static_cast<int>(cuda::devices.size());
auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<cuda::std::int32_t>> send;
std::vector<cuda::device_buffer<cuda::std::int32_t>> recv;
for (int i = 0; i < n; ++i)
{
auto pool = cuda::device_default_memory_pool(cuda::devices[i]);
auto& s = send.emplace_back(cuda::make_buffer(streams[i], pool, 3, i));
recv.emplace_back(cuda::make_buffer<cuda::std::int32_t>(streams[i], pool, s.size(), -1));
}
{
auto g = this->communicators().front().group_guard();
for (int i = 0; i < n; ++i)
{
const int prev = (i + n - 1) % n;
const int next = (i + 1) % n;
this->communicators()[i].recv(g, recv[i].data(), recv[i].size(), prev, streams[i]);
this->communicators()[i].send(g, send[i].data(), send[i].size(), next, streams[i]);
}
}
for (auto& stream : streams)
{
stream.sync();
}
// Rank r received from its predecessor (r-1): {r-1, r-1, r-1}.
for (int r = 0; r < n; ++r)
{
const cuda::std::int32_t prev = (r + n - 1) % n;
auto pool = cuda::mr::legacy_pinned_memory_resource{};
const cuda::host_buffer<cuda::std::int32_t> expected =
cuda::make_buffer(recv[r].stream(), pool, recv[r].size(), prev);
const cuda::host_buffer<cuda::std::int32_t> actual = cuda::make_buffer(recv[r].stream(), pool, recv[r]);
REQUIRE_THAT(actual, Equals(expected));
}
}
MULTI_GPU_TEST("nccl_communicator_ref send/recv transports trivially copyable payload", )
{
if (cuda::devices.size() == 1)
{
// NCCL disallows self send/recv on a 1-rank comm.
REQUIRE(this->communicators().front().rank() == 0);
REQUIRE(this->communicators().front().size() == 1);
return;
}
const int n = static_cast<int>(cuda::devices.size());
auto streams = nccl_test_util::make_streams();
std::vector<cuda::device_buffer<payload>> send;
std::vector<cuda::device_buffer<payload>> recv;
for (int i = 0; i < n; ++i)
{
auto pool = cuda::device_default_memory_pool(cuda::devices[i]);
std::vector<payload> h(3);
for (cuda::std::size_t k = 0; k < h.size(); ++k)
{
h[k] = payload{static_cast<cuda::std::int32_t>(i), static_cast<cuda::std::int32_t>(k)};
}
auto& s = send.emplace_back(streams[i], pool, h);
recv.emplace_back(cuda::make_buffer<payload>(streams[i], pool, s.size(), cuda::no_init));
}
{
auto g = this->communicators().front().group_guard();
for (int i = 0; i < n; ++i)
{
const int prev = (i + n - 1) % n;
const int next = (i + 1) % n;
this->communicators()[i].recv(g, recv[i].data(), recv[i].size(), prev, streams[i]);
this->communicators()[i].send(g, send[i].data(), send[i].size(), next, streams[i]);
}
}
for (auto& stream : streams)
{
stream.sync();
}
for (int r = 0; r < n; ++r)
{
const int prev = (r + n - 1) % n;
auto pool = cuda::mr::legacy_pinned_memory_resource{};
const cuda::host_buffer<payload> actual = cuda::make_buffer(recv[r].stream(), pool, recv[r]);
actual.stream().sync();
for (cuda::std::size_t k = 0; k < actual.size(); ++k)
{
REQUIRE(actual[k].from == static_cast<cuda::std::int32_t>(prev));
REQUIRE(actual[k].index == static_cast<cuda::std::int32_t>(k));
}
}
}

View File

@@ -1,15 +0,0 @@
#===----------------------------------------------------------------------===##
#
# 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.
#
#===----------------------------------------------------------------------===##
file(GLOB test_srcs LIST_DIRECTORIES FALSE CONFIGURE_DEPENDS *.cu *.cpp)
foreach (src IN LISTS test_srcs)
cudax_add_multi_gpu_test("concepts" test_target "${src}")
endforeach()

View File

@@ -1,66 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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.
//
//===----------------------------------------------------------------------===//
#ifndef _CUDAX_TEST_MULTI_GPU_COLLECTIVE_CONCEPTS_COMMON_CUH
#define _CUDAX_TEST_MULTI_GPU_COLLECTIVE_CONCEPTS_COMMON_CUH
#include <cuda/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/std/cstdint>
#include "concepts_common.cuh"
namespace cudax_multi_gpu_concepts
{
struct collective_communicator_model : communicator_model
{
template <class Tp, class Op>
void reduce(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, Op, ::cuda::std::int32_t, ::cuda::stream_ref);
template <class Tp, class Op>
void all_reduce(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, Op, ::cuda::stream_ref);
template <class Tp>
void gather(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t, ::cuda::stream_ref);
template <class Tp>
void gather_v(
group_guard_type&,
Tp*,
::cuda::std::size_t,
Tp*,
const ::cuda::std::size_t*,
const ::cuda::std::size_t*,
::cuda::std::int32_t,
::cuda::stream_ref);
template <class Tp>
void all_gather(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, ::cuda::stream_ref);
template <class Tp>
void broadcast(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t, ::cuda::stream_ref);
template <class Tp>
void all_to_all(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, ::cuda::stream_ref);
template <class Tp>
void all_to_all_v(
group_guard_type&,
Tp*,
const ::cuda::std::size_t*,
const ::cuda::std::size_t*,
Tp*,
const ::cuda::std::size_t*,
const ::cuda::std::size_t*,
::cuda::stream_ref);
};
} // namespace cudax_multi_gpu_concepts
#endif // _CUDAX_TEST_MULTI_GPU_COLLECTIVE_CONCEPTS_COMMON_CUH

View File

@@ -1,44 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/experimental/__multi_gpu/concepts.h>
#include <testing.cuh>
#include "concepts_common.cuh"
namespace
{
namespace types = cudax_multi_gpu_concepts;
// nvcc ignores [[maybe_unused]] entirely
_CCCL_BEGIN_NV_DIAG_SUPPRESS(177)
struct no_send : types::basic_communicator_model
{
template <class Tp>
void recv(group_guard_type&, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t, ::cuda::stream_ref);
};
struct no_recv : types::basic_communicator_model
{
template <class Tp>
void send(group_guard_type&, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t, ::cuda::stream_ref);
};
_CCCL_END_NV_DIAG_SUPPRESS()
} // namespace
C2H_TEST("communicator concept", "[multi_gpu][concepts]")
{
STATIC_REQUIRE(cudax::__communicator<types::communicator_model>);
STATIC_REQUIRE(!cudax::__communicator<no_send>);
STATIC_REQUIRE(!cudax::__communicator<no_recv>);
}

View File

@@ -1,43 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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.
//
//===----------------------------------------------------------------------===//
#ifndef _CUDAX_TEST_MULTI_GPU_CONCEPTS_COMMON_CUH
#define _CUDAX_TEST_MULTI_GPU_CONCEPTS_COMMON_CUH
#include <cuda/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/std/cstdint>
namespace cudax_multi_gpu_concepts
{
struct group_guard
{};
struct basic_communicator_model
{
using native_handle_type = int;
using group_guard_type = group_guard;
native_handle_type native_handle() noexcept;
::cuda::std::int32_t rank() noexcept;
::cuda::std::int32_t size() noexcept;
group_guard_type group_guard();
};
struct communicator_model : basic_communicator_model
{
template <class Tp>
void send(group_guard_type&, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t, ::cuda::stream_ref);
template <class Tp>
void recv(group_guard_type&, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t, ::cuda::stream_ref);
};
} // namespace cudax_multi_gpu_concepts
#endif // _CUDAX_TEST_MULTI_GPU_CONCEPTS_COMMON_CUH

View File

@@ -1,43 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/experimental/__multi_gpu/concepts.h>
#include <testing.cuh>
#include "collective_concepts_common.cuh"
namespace
{
namespace types = cudax_multi_gpu_concepts;
// nvcc ignores [[maybe_unused]] entirely
_CCCL_BEGIN_NV_DIAG_SUPPRESS(177)
struct all_gather_returns_int : types::communicator_model
{
template <class Tp>
int all_gather(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, ::cuda::stream_ref);
};
_CCCL_END_NV_DIAG_SUPPRESS()
} // namespace
C2H_TEST("__has_all_gather concept", "[multi_gpu][concepts]")
{
STATIC_REQUIRE(cudax::__has_all_gather<types::collective_communicator_model>);
STATIC_REQUIRE(cudax::__has_all_gather<types::collective_communicator_model, long*>);
STATIC_REQUIRE(!cudax::__has_all_gather<types::communicator_model>);
STATIC_REQUIRE(!cudax::__has_all_gather<all_gather_returns_int>);
}

View File

@@ -1,53 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/std/__type_traits/enable_if.h>
#include <cuda/std/__type_traits/is_void.h>
#include <cuda/experimental/__multi_gpu/concepts.h>
#include <testing.cuh>
#include "collective_concepts_common.cuh"
namespace
{
namespace types = cudax_multi_gpu_concepts;
// nvcc ignores [[maybe_unused]] entirely
_CCCL_BEGIN_NV_DIAG_SUPPRESS(177)
struct all_reduce_rejects_void : types::communicator_model
{
template <class Tp, class Op, ::cuda::std::enable_if_t<!::cuda::std::is_void_v<Tp>, int> = 0>
void all_reduce(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, Op, ::cuda::stream_ref);
};
struct all_reduce_returns_int : types::communicator_model
{
template <class Tp, class Op>
int all_reduce(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, Op, ::cuda::stream_ref);
};
_CCCL_END_NV_DIAG_SUPPRESS()
} // namespace
C2H_TEST("__has_all_reduce concept", "[multi_gpu][concepts]")
{
STATIC_REQUIRE(cudax::__has_all_reduce<types::collective_communicator_model>);
STATIC_REQUIRE(cudax::__has_all_reduce<types::collective_communicator_model, long*>);
STATIC_REQUIRE(!cudax::__has_all_reduce<types::communicator_model>);
STATIC_REQUIRE(!cudax::__has_all_reduce<all_reduce_returns_int>);
STATIC_REQUIRE(cudax::__has_all_reduce<all_reduce_rejects_void, int*>);
STATIC_REQUIRE(!cudax::__has_all_reduce<all_reduce_rejects_void, void*>);
}

View File

@@ -1,43 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/experimental/__multi_gpu/concepts.h>
#include <testing.cuh>
#include "collective_concepts_common.cuh"
namespace
{
namespace types = cudax_multi_gpu_concepts;
// nvcc ignores [[maybe_unused]] entirely
_CCCL_BEGIN_NV_DIAG_SUPPRESS(177)
struct all_to_all_returns_int : types::communicator_model
{
template <class Tp>
int all_to_all(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, ::cuda::stream_ref);
};
_CCCL_END_NV_DIAG_SUPPRESS()
} // namespace
C2H_TEST("__has_all_to_all concept", "[multi_gpu][concepts]")
{
STATIC_REQUIRE(cudax::__has_all_to_all<types::collective_communicator_model>);
STATIC_REQUIRE(cudax::__has_all_to_all<types::collective_communicator_model, long*>);
STATIC_REQUIRE(!cudax::__has_all_to_all<types::communicator_model>);
STATIC_REQUIRE(!cudax::__has_all_to_all<all_to_all_returns_int>);
}

View File

@@ -1,51 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/experimental/__multi_gpu/concepts.h>
#include <testing.cuh>
#include "collective_concepts_common.cuh"
namespace
{
namespace types = cudax_multi_gpu_concepts;
// nvcc ignores [[maybe_unused]] entirely
_CCCL_BEGIN_NV_DIAG_SUPPRESS(177)
struct all_to_all_v_returns_int : types::communicator_model
{
template <class Tp>
int all_to_all_v(
group_guard_type&,
Tp*,
const ::cuda::std::size_t*,
const ::cuda::std::size_t*,
Tp*,
const ::cuda::std::size_t*,
const ::cuda::std::size_t*,
::cuda::stream_ref);
};
_CCCL_END_NV_DIAG_SUPPRESS()
} // namespace
C2H_TEST("__has_all_to_all_v concept", "[multi_gpu][concepts]")
{
STATIC_REQUIRE(cudax::__has_all_to_all_v<types::collective_communicator_model>);
STATIC_REQUIRE(cudax::__has_all_to_all_v<types::collective_communicator_model, long*>);
STATIC_REQUIRE(!cudax::__has_all_to_all_v<types::communicator_model>);
STATIC_REQUIRE(!cudax::__has_all_to_all_v<all_to_all_v_returns_int>);
}

View File

@@ -1,44 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/std/cstdint>
#include <cuda/experimental/__multi_gpu/concepts.h>
#include <testing.cuh>
#include "collective_concepts_common.cuh"
namespace
{
namespace types = cudax_multi_gpu_concepts;
// nvcc ignores [[maybe_unused]] entirely
_CCCL_BEGIN_NV_DIAG_SUPPRESS(177)
struct broadcast_returns_int : types::communicator_model
{
template <class Tp>
int broadcast(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t, ::cuda::stream_ref);
};
_CCCL_END_NV_DIAG_SUPPRESS()
} // namespace
C2H_TEST("__has_broadcast concept", "[multi_gpu][concepts]")
{
STATIC_REQUIRE(cudax::__has_broadcast<types::collective_communicator_model>);
STATIC_REQUIRE(cudax::__has_broadcast<types::collective_communicator_model, long*>);
STATIC_REQUIRE(!cudax::__has_broadcast<types::communicator_model>);
STATIC_REQUIRE(!cudax::__has_broadcast<broadcast_returns_int>);
}

View File

@@ -1,44 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/std/cstdint>
#include <cuda/experimental/__multi_gpu/concepts.h>
#include <testing.cuh>
#include "collective_concepts_common.cuh"
namespace
{
namespace types = cudax_multi_gpu_concepts;
// nvcc ignores [[maybe_unused]] entirely
_CCCL_BEGIN_NV_DIAG_SUPPRESS(177)
struct gather_returns_int : types::communicator_model
{
template <class Tp>
int gather(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t, ::cuda::stream_ref);
};
_CCCL_END_NV_DIAG_SUPPRESS()
} // namespace
C2H_TEST("__has_gather concept", "[multi_gpu][concepts]")
{
STATIC_REQUIRE(cudax::__has_gather<types::collective_communicator_model>);
STATIC_REQUIRE(cudax::__has_gather<types::collective_communicator_model, long*>);
STATIC_REQUIRE(!cudax::__has_gather<types::communicator_model>);
STATIC_REQUIRE(!cudax::__has_gather<gather_returns_int>);
}

View File

@@ -1,51 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/std/cstdint>
#include <cuda/experimental/__multi_gpu/concepts.h>
#include <testing.cuh>
#include "collective_concepts_common.cuh"
namespace
{
namespace types = cudax_multi_gpu_concepts;
// nvcc ignores [[maybe_unused]] entirely
_CCCL_BEGIN_NV_DIAG_SUPPRESS(177)
struct gather_v_returns_int : types::communicator_model
{
template <class Tp>
int gather_v(group_guard_type&,
Tp*,
::cuda::std::size_t,
Tp*,
const ::cuda::std::size_t*,
const ::cuda::std::size_t*,
::cuda::std::int32_t,
::cuda::stream_ref);
};
_CCCL_END_NV_DIAG_SUPPRESS()
} // namespace
C2H_TEST("__has_gather_v concept", "[multi_gpu][concepts]")
{
STATIC_REQUIRE(cudax::__has_gather_v<types::collective_communicator_model>);
STATIC_REQUIRE(cudax::__has_gather_v<types::collective_communicator_model, long*>);
STATIC_REQUIRE(!cudax::__has_gather_v<types::communicator_model>);
STATIC_REQUIRE(!cudax::__has_gather_v<gather_v_returns_int>);
}

View File

@@ -1,50 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/std/cstdint>
#include <cuda/experimental/__multi_gpu/concepts.h>
#include <testing.cuh>
#include "concepts_common.cuh"
namespace
{
namespace types = cudax_multi_gpu_concepts;
// nvcc ignores [[maybe_unused]] entirely
_CCCL_BEGIN_NV_DIAG_SUPPRESS(177)
struct recv_returns_int : types::basic_communicator_model
{
template <class Tp>
int recv(group_guard_type&, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t, ::cuda::stream_ref);
};
struct no_recv : types::basic_communicator_model
{
template <class Tp>
void send(group_guard_type&, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t, ::cuda::stream_ref);
};
_CCCL_END_NV_DIAG_SUPPRESS()
} // namespace
C2H_TEST("__has_recv concept", "[multi_gpu][concepts]")
{
STATIC_REQUIRE(cudax::__has_recv<types::communicator_model>);
STATIC_REQUIRE(cudax::__has_recv<types::communicator_model, long*>);
STATIC_REQUIRE(!cudax::__has_recv<no_recv>);
STATIC_REQUIRE(!cudax::__has_recv<recv_returns_int>);
}

View File

@@ -1,44 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/std/cstdint>
#include <cuda/experimental/__multi_gpu/concepts.h>
#include <testing.cuh>
#include "collective_concepts_common.cuh"
namespace
{
namespace types = cudax_multi_gpu_concepts;
// nvcc ignores [[maybe_unused]] entirely
_CCCL_BEGIN_NV_DIAG_SUPPRESS(177)
struct reduce_returns_int : types::communicator_model
{
template <class Tp, class Op>
int reduce(group_guard_type&, Tp*, Tp*, ::cuda::std::size_t, Op, ::cuda::std::int32_t, ::cuda::stream_ref);
};
_CCCL_END_NV_DIAG_SUPPRESS()
} // namespace
C2H_TEST("__has_reduce concept", "[multi_gpu][concepts]")
{
STATIC_REQUIRE(cudax::__has_reduce<types::collective_communicator_model>);
STATIC_REQUIRE(cudax::__has_reduce<types::collective_communicator_model, long*>);
STATIC_REQUIRE(!cudax::__has_reduce<types::communicator_model>);
STATIC_REQUIRE(!cudax::__has_reduce<reduce_returns_int>);
}

View File

@@ -1,50 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/__stream/stream_ref.h>
#include <cuda/std/__cstddef/types.h>
#include <cuda/std/cstdint>
#include <cuda/experimental/__multi_gpu/concepts.h>
#include <testing.cuh>
#include "concepts_common.cuh"
namespace
{
namespace types = cudax_multi_gpu_concepts;
// nvcc ignores [[maybe_unused]] entirely
_CCCL_BEGIN_NV_DIAG_SUPPRESS(177)
struct no_send : types::basic_communicator_model
{
template <class Tp>
void send_sync(group_guard_type&, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t);
};
struct send_returns_int : no_send
{
template <class Tp>
int send(group_guard_type&, Tp*, ::cuda::std::size_t, ::cuda::std::int32_t, ::cuda::stream_ref);
};
_CCCL_END_NV_DIAG_SUPPRESS()
} // namespace
C2H_TEST("__has_send concept", "[multi_gpu][concepts]")
{
STATIC_REQUIRE(cudax::__has_send<types::communicator_model>);
STATIC_REQUIRE(cudax::__has_send<types::communicator_model, long*>);
STATIC_REQUIRE(!cudax::__has_send<no_send>);
STATIC_REQUIRE(!cudax::__has_send<send_returns_int>);
}

View File

@@ -1,76 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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.
//
//===----------------------------------------------------------------------===//
#pragma once
#include <cuda/buffer>
#include <cuda/std/cstddef>
#include <exception>
#include <future>
#include <vector>
// One output iterator per local output buffer. Collected after `out` is fully built so the
// iterators do not dangle across reallocations.
template <class T>
[[nodiscard]] std::vector<typename cuda::device_buffer<T>::iterator>
make_output_iterators(std::vector<cuda::device_buffer<T>>& out)
{
std::vector<typename cuda::device_buffer<T>::iterator> outputs;
outputs.reserve(out.size());
for (auto& buf : out)
{
outputs.push_back(buf.begin());
}
return outputs;
}
template <class Fn>
void run_threaded(cuda::std::size_t num_ranks, Fn fn)
{
// Every rank must be launched before any is waited on: the single-communicator `reduce`
// blocks on a collective, so calling `get()` on rank 0's future before rank 1 is even
// started would deadlock. Launch all futures into the vector first, then drain them.
std::vector<std::future<void>> futures;
futures.reserve(num_ranks);
for (cuda::std::size_t i = 0; i < num_ranks; ++i)
{
futures.push_back(std::async(std::launch::async, fn, i));
}
// `std::async` stashes any exception thrown by `fn` in the future and `get()` rethrows it on
// the main thread, where Catch2 can report it as a normal failure. Any not-yet-drained
// future still joins its thread in its destructor, so a throw here never leaves a peer
// waiting on an unposted collective. Drain every future so a failure on rank 0 does not mask
// one on a peer.
std::exception_ptr error = nullptr;
for (auto& f : futures)
{
try
{
f.get();
}
catch (...)
{
if (!error)
{
error = std::current_exception();
}
}
}
if (error)
{
std::rethrow_exception(error);
}
}

View File

@@ -1,94 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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.
//
//===----------------------------------------------------------------------===//
#ifndef CUDAX_TEST_MULTI_NCCL_TEST_COMMON_H
#define CUDAX_TEST_MULTI_NCCL_TEST_COMMON_H
#include <cuda/devices>
#include <cuda/std/cstddef>
#include <cuda/std/span>
#include <cuda/experimental/__multi_gpu/nccl_communicator.h>
#include <cuda/experimental/__multi_gpu/nccl_communicator_ref.h>
#include <cuda/experimental/stream.cuh>
#include <vector>
#include <nccl.h>
#include <c2h/catch2_test_helper.h>
namespace cudax = ::cuda::experimental;
namespace nccl_test_util
{
// One stream per rank, each current on its own device.
[[nodiscard]] inline std::vector<cudax::stream> make_streams()
{
return {cuda::devices.begin(), cuda::devices.end()};
}
[[nodiscard]] inline const std::vector<cudax::nccl_communicator>& nccl_comms()
{
static const auto comms = []() -> std::vector<cudax::nccl_communicator> {
if (cuda::devices.size() == 0)
{
SKIP("No CUDA devices visible");
}
std::vector<int> devs;
devs.reserve(cuda::devices.size());
for (auto d : cuda::devices)
{
devs.emplace_back(d.get());
}
std::vector<ncclComm_t> raw_comms(devs.size());
const ncclResult_t result = ncclCommInitAll(raw_comms.data(), static_cast<int>(devs.size()), devs.data());
INFO("NCCL: " << ncclGetErrorString(result));
REQUIRE(result == ncclSuccess);
std::vector<cudax::nccl_communicator> comms;
comms.reserve(raw_comms.size());
for (const auto comm : raw_comms)
{
comms.emplace_back(cudax::nccl_communicator::from_native_handle(comm));
}
return comms;
}();
return comms;
}
// Caches a single-process, multi-GPU NCCL communicator world for the life of the entire test
// suite.
template <class = void>
class nccl_comm_fixture
{
public:
[[nodiscard]] cuda::std::span<cudax::nccl_communicator_ref> communicators()
{
return wrappers_;
}
private:
std::vector<cudax::nccl_communicator_ref> wrappers_{nccl_comms().begin(), nccl_comms().end()};
};
#define MULTI_GPU_TEST(NAME, ...) \
C2H_TEST_WITH_FIXTURE(::nccl_test_util::nccl_comm_fixture, NAME, "[multi_gpu][nccl]", __VA_ARGS__)
} // namespace nccl_test_util
#endif // CUDAX_TEST_MULTI_GPU_NCCL_TEST_COMMON_H

View File

@@ -1,15 +0,0 @@
#===----------------------------------------------------------------------===##
#
# 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.
#
#===----------------------------------------------------------------------===##
file(GLOB test_srcs LIST_DIRECTORIES FALSE CONFIGURE_DEPENDS *.cu *.cpp)
foreach (src IN LISTS test_srcs)
cudax_add_multi_gpu_test("nccl" test_target "${src}")
endforeach()

View File

@@ -1,129 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/std/__type_traits/is_same.h>
#include <cuda/std/__type_traits/underlying_type.h>
#include <cuda/experimental/__nccl/abi_compatible.h>
#include <c2h/catch2_test_helper.h>
namespace
{
namespace abi_detail = ::cuda::experimental::__nccl::__abi_detail;
// NOLINTBEGIN(bugprone-reserved-identifier)
enum Foo_enum
{
};
#if _CCCL_OS(WINDOWS)
using FooEnum = int;
#else
using FooEnum = unsigned int;
#endif
struct Foo_st;
using FooStruct = Foo_st*;
// An enum whose underlying type is fixed, so that mismatch tests are deterministic
// regardless of how the implementation picks the underlying type of an unfixed enum.
enum class CharEnum : char
{
};
enum class IntEnum : int
{
};
struct Bar_st;
// NOLINTEND(bugprone-reserved-identifier)
} // namespace
C2H_TEST("nccl __abi_compatible type comparisons", "[multi_gpu][nccl]")
{
// --- Identical / scalar types -------------------------------------------------
STATIC_REQUIRE(abi_detail::__abi_compatible<int, int>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<int, float>());
// remove_cv only strips the top level, so a top-level cv difference is still compatible.
STATIC_REQUIRE(abi_detail::__abi_compatible<const int, int>());
STATIC_REQUIRE(abi_detail::__abi_compatible<volatile int, int>());
STATIC_REQUIRE(abi_detail::__abi_compatible<const volatile int, int>());
// Different-width / signedness integers are distinct types and must not be compatible.
STATIC_REQUIRE(!abi_detail::__abi_compatible<int, long>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<int, short>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<int, unsigned int>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<char, signed char>());
// --- Pointers -----------------------------------------------------------------
STATIC_REQUIRE(abi_detail::__abi_compatible<const char**, const char* const*>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<const char*, const int*>());
// Deep cv-qualifications on the pointee are stripped at each level of recursion.
STATIC_REQUIRE(abi_detail::__abi_compatible<int*, const int*>());
STATIC_REQUIRE(abi_detail::__abi_compatible<int**, const int* const*>());
// Multi-level pointers must agree in depth.
STATIC_REQUIRE(abi_detail::__abi_compatible<int**, int**>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<int**, int*>());
// A pointer is never compatible with a non-pointer (the `&&` in the pointer branch).
STATIC_REQUIRE(!abi_detail::__abi_compatible<int*, int>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<int, int*>());
// void* is not compatible with a typed pointer: the pointees differ.
STATIC_REQUIRE(!abi_detail::__abi_compatible<void*, int*>());
STATIC_REQUIRE(abi_detail::__abi_compatible<void*, void*>());
// Opaque struct pointers: identical handle types and matching opaque pointees.
STATIC_REQUIRE(abi_detail::__abi_compatible<Foo_st*, FooStruct>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<Foo_st*, Bar_st*>());
// --- Enums --------------------------------------------------------------------
STATIC_REQUIRE(::cuda::std::is_same_v<FooEnum, ::cuda::std::underlying_type_t<Foo_enum>>);
// enum vs its underlying type (either side), and enum-vs-enum through the pointee.
STATIC_REQUIRE(abi_detail::__abi_compatible<FooEnum, Foo_enum>());
STATIC_REQUIRE(abi_detail::__abi_compatible<Foo_enum, FooEnum>());
STATIC_REQUIRE(abi_detail::__abi_compatible<FooEnum*, Foo_enum*>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<FooEnum*, Foo_enum>());
// Both sides enums: compatible iff their underlying types match exactly.
STATIC_REQUIRE(abi_detail::__abi_compatible<CharEnum, CharEnum>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<CharEnum, IntEnum>());
// An enum is compatible with its exact underlying type but not a mismatched one.
STATIC_REQUIRE(abi_detail::__abi_compatible<CharEnum, char>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<CharEnum, int>());
STATIC_REQUIRE(abi_detail::__abi_compatible<IntEnum, int>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<IntEnum, char>());
// --- Function pointers --------------------------------------------------------
STATIC_REQUIRE(abi_detail::__abi_compatible<int (*)(Foo_st*), int (*)(FooStruct)>());
// Return type, arity, and argument types must all match.
STATIC_REQUIRE(!abi_detail::__abi_compatible<int (*)(int), float (*)(int)>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<int (*)(int), int (*)(int, int)>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<int (*)(int), int (*)(long)>());
// void return and no-argument functions.
STATIC_REQUIRE(abi_detail::__abi_compatible<void (*)(), void (*)()>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<void (*)(), int (*)()>());
// Per-argument ABI compatibility recurses (enum / opaque-pointer arguments).
STATIC_REQUIRE(abi_detail::__abi_compatible<void (*)(FooEnum), void (*)(Foo_enum)>());
STATIC_REQUIRE(abi_detail::__abi_compatible<int (*)(Foo_st*, FooEnum), int (*)(FooStruct, Foo_enum)>());
STATIC_REQUIRE(!abi_detail::__abi_compatible<void (*)(CharEnum), void (*)(IntEnum)>());
// A function type is not compatible with a non-function type.
STATIC_REQUIRE(!abi_detail::__abi_compatible<int (*)(int), int*>());
}

View File

@@ -1,100 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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/functional> // cuda::maximum, cuda::minimum
#include <cuda/std/cstdint>
#include <cuda/std/functional> // cuda::std::plus, cuda::std::multiplies
#include <cuda/experimental/__nccl/nccl_api.h>
#include <c2h/catch2_test_helper.h>
namespace
{
// A trivially-copyable aggregate with no corresponding NCCL data type.
struct trivial_aggregate
{
int a;
double b;
};
// A non-trivially-copyable type (user-provided copy ctor).
struct non_trivial
{
non_trivial(const non_trivial&) {} // NOLINT(modernize-use-equals-default)
int a;
};
} // namespace
C2H_TEST("nccl type and reduction-op traits", "[multi_gpu][nccl]")
{
namespace nccl = ::cuda::experimental::__nccl;
// --- __has_nccl_type_of -------------------------------------------------------
//
// True only for the fixed set of types __nccl_type_of() recognizes.
STATIC_REQUIRE(nccl::__has_nccl_type_of<::cuda::std::int8_t>);
STATIC_REQUIRE(nccl::__has_nccl_type_of<::cuda::std::uint8_t>);
STATIC_REQUIRE(nccl::__has_nccl_type_of<::cuda::std::int32_t>);
STATIC_REQUIRE(nccl::__has_nccl_type_of<::cuda::std::uint32_t>);
STATIC_REQUIRE(nccl::__has_nccl_type_of<::cuda::std::int64_t>);
STATIC_REQUIRE(nccl::__has_nccl_type_of<::cuda::std::uint64_t>);
STATIC_REQUIRE(nccl::__has_nccl_type_of<float>);
STATIC_REQUIRE(nccl::__has_nccl_type_of<double>);
STATIC_REQUIRE(nccl::__has_nccl_type_of<bool>);
// cv-qualifications are stripped via remove_cvref_t before the lookup.
STATIC_REQUIRE(nccl::__has_nccl_type_of<const float>);
STATIC_REQUIRE(nccl::__has_nccl_type_of<volatile int>);
STATIC_REQUIRE(nccl::__has_nccl_type_of<float&>);
// Unsupported types have no NCCL data type.
STATIC_REQUIRE(!nccl::__has_nccl_type_of<long double>);
STATIC_REQUIRE(!nccl::__has_nccl_type_of<void>);
STATIC_REQUIRE(!nccl::__has_nccl_type_of<int*>);
STATIC_REQUIRE(!nccl::__has_nccl_type_of<trivial_aggregate>);
// --- __nccl_type_of_v ---------------------------------------------------------
STATIC_REQUIRE(nccl::__nccl_type_of_v<bool> == nccl::__ncclChar);
STATIC_REQUIRE(nccl::__nccl_type_of_v<::cuda::std::int8_t> == nccl::__ncclInt8);
STATIC_REQUIRE(nccl::__nccl_type_of_v<::cuda::std::uint8_t> == nccl::__ncclUint8);
STATIC_REQUIRE(nccl::__nccl_type_of_v<::cuda::std::int32_t> == nccl::__ncclInt32);
STATIC_REQUIRE(nccl::__nccl_type_of_v<::cuda::std::uint32_t> == nccl::__ncclUint32);
STATIC_REQUIRE(nccl::__nccl_type_of_v<::cuda::std::int64_t> == nccl::__ncclInt64);
STATIC_REQUIRE(nccl::__nccl_type_of_v<::cuda::std::uint64_t> == nccl::__ncclUint64);
STATIC_REQUIRE(nccl::__nccl_type_of_v<float> == nccl::__ncclFloat);
STATIC_REQUIRE(nccl::__nccl_type_of_v<double> == nccl::__ncclDouble);
// cv-ref qualifiers are removed before mapping.
STATIC_REQUIRE(nccl::__nccl_type_of_v<const double&> == nccl::__ncclDouble);
// --- __nccl_redop_of_v --------------------------------------------------------
STATIC_REQUIRE(nccl::__nccl_redop_of_v<::cuda::std::plus<>> == nccl::__ncclSum);
STATIC_REQUIRE(nccl::__nccl_redop_of_v<::cuda::std::multiplies<>> == nccl::__ncclProd);
STATIC_REQUIRE(nccl::__nccl_redop_of_v<::cuda::maximum<>> == nccl::__ncclMax);
STATIC_REQUIRE(nccl::__nccl_redop_of_v<::cuda::minimum<>> == nccl::__ncclMin);
STATIC_REQUIRE(nccl::__nccl_redop_of_v<const ::cuda::std::plus<int>&> == nccl::__ncclSum);
// --- __has_nccl_redop ---------------------------------------------------------
STATIC_REQUIRE(nccl::__has_nccl_redop_of<::cuda::std::plus<>>);
STATIC_REQUIRE(nccl::__has_nccl_redop_of<::cuda::std::multiplies<>>);
STATIC_REQUIRE(nccl::__has_nccl_redop_of<::cuda::maximum<>>);
STATIC_REQUIRE(nccl::__has_nccl_redop_of<::cuda::minimum<>>);
// Typed specializations are recognized too.
STATIC_REQUIRE(nccl::__has_nccl_redop_of<::cuda::std::plus<int>>);
// cv-ref qualifiers are stripped before the lookup.
STATIC_REQUIRE(nccl::__has_nccl_redop_of<const ::cuda::std::plus<>&>);
// Unsupported functors and non-functor types are rejected.
STATIC_REQUIRE(!nccl::__has_nccl_redop_of<::cuda::std::minus<>>);
STATIC_REQUIRE(!nccl::__has_nccl_redop_of<int>);
}