Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cub/cub/agent/agent_reduce_by_key.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,12 @@ namespace detail::reduce_by_key
*
* @tparam AccumT
* The type of intermediate accumulator (according to P2322R6)
*
* @tparam StableReductionOrder
* Whether to use a stable reduction order across tiles
*/
template <typename AgentReduceByKeyPolicyT,
bool StableReductionOrder,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: let's please keep the ordering of the template parameters consistent, so this should go at the end of the template parameter list

typename KeysInputIteratorT,
typename UniqueOutputIteratorT,
typename ValuesInputIteratorT,
Expand Down Expand Up @@ -230,7 +234,7 @@ struct AgentReduceByKey
// Callback type for obtaining tile prefix during block scan
using DelayConstructorT = typename AgentReduceByKeyPolicyT::detail::delay_constructor_t;
using TilePrefixCallbackOpT =
TilePrefixCallbackOp<OffsetValuePairT, ReduceBySegmentOpT, ScanTileStateT, DelayConstructorT>;
TilePrefixCallbackOp<OffsetValuePairT, ReduceBySegmentOpT, ScanTileStateT, DelayConstructorT, StableReductionOrder>;

// Key and value exchange types
using KeyExchangeT = KeyOutputT[TILE_ITEMS + 1];
Expand Down
74 changes: 60 additions & 14 deletions cub/cub/device/device_reduce.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#include <cuda/__memory_resource/get_memory_resource.h>
#include <cuda/__stream/get_stream.h>
#include <cuda/__stream/stream_ref.h>
#include <cuda/__type_traits/is_floating_point.h>
#include <cuda/argument>
#include <cuda/std/__execution/env.h>
#include <cuda/std/__functional/identity.h>
Expand Down Expand Up @@ -153,7 +154,9 @@ inline constexpr bool is_non_deterministic_v =
//! ====================================
//!
//! ``cub::DeviceReduce`` supports all three :ref:`determinism guarantees <cccl-determinism>`; the
//! default is ``run_to_run``.
//! default is ``run_to_run``. ``ReduceByKey`` has separate type/operator constraints, documented on its
//! environment overload below. The implementation details below apply to the other reductions;
//! ``ReduceByKey`` does not use RFA or atomic accumulation.
//!
//! - ``run_to_run`` (the default) is reproducible because, for a given GPU, every launch with the same input,
//! build, and launch configuration selects the *same* tuning policy and therefore performs the *same* fixed
Expand Down Expand Up @@ -2734,15 +2737,26 @@ public:
//! respectively. The total number of runs encountered is written to ``d_num_runs_out``.
//!
//! - The ``==`` equality operator is used to determine whether keys are equivalent
//! - Provides "run-to-run" determinism for pseudo-associative reduction
//! (e.g., addition of floating point types) on the same GPU device.
//! - Requests "run-to-run" determinism by default. It is supported for integral types with known operators,
//! primitive types with min/max, and floating-point types with ``cuda::std::plus``.
//! - Pass ``cuda::execution::require(cuda::execution::determinism::not_guaranteed)`` in the environment to opt out.
//! This removes the determinism requirement without changing the algorithm's other requirements.
//! - "gpu-to-gpu" determinism is supported for integral types with known operators and primitive types with min/max.
//! Floating-point addition is unsupported; ``ReduceByKey`` does not use RFA.
//! Other combinations under "run-to-run" or "gpu-to-gpu" are rejected at compile time.
Comment thread
pauleonix marked this conversation as resolved.
Resolved
//! - Let ``out`` be any of
//! ``[d_unique_out, d_unique_out + *d_num_runs_out)``
//! ``[d_aggregates_out, d_aggregates_out + *d_num_runs_out)``
//! ``d_num_runs_out``. The ranges represented by ``out`` shall not overlap
//! ``[d_keys_in, d_keys_in + num_items)``,
//! ``[d_values_in, d_values_in + num_items)`` nor ``out`` in any way.
//!
//! These determinism constraints apply to this environment overload and its accumulator type. Known CUDA binary
//! operators are ``cuda::minimum``, ``cuda::maximum``, and ``cuda::std::plus``, ``multiplies``, ``bit_and``,
//! ``bit_or``, ``bit_xor``, ``logical_and``, and ``logical_or``. Primitive accumulators are the types recognized by
//! ``cub::Traits``; floating-point accumulators (including CUDA extended types) are recognized by
//! ``cuda::is_floating_point_v``. The operator must support the accumulator type.
//!
//! Snippet
//! +++++++++++++++++++++++++++++++++++++++++++++
//!
Expand Down Expand Up @@ -2780,7 +2794,8 @@ public:
//!
//! @tparam EnvT
//! **[inferred]** Execution environment type. Default is ``cuda::std::execution::env<>``.
//! Supports customization of stream via ``cuda::get_stream``.
//! Supports customization of stream via ``cuda::get_stream`` and determinism through
//! ``cuda::execution::require``.
//!
//! @param[in] d_keys_in
//! Pointer to the input sequence of keys
Expand Down Expand Up @@ -2830,15 +2845,43 @@ public:
{
_CCCL_NVTX_RANGE_SCOPE("cub::DeviceReduce::ReduceByKey");

using OffsetT = detail::choose_offset_t<NumItemsT>;
using EqualityOp = ::cuda::std::equal_to<>;
using default_policy_selector = detail::reduce_by_key::policy_selector_from_types<
ReductionOpT,
::cuda::std::__accumulator_t<ReductionOpT, detail::it_value_t<ValuesInputIteratorT>>,
detail::non_void_value_t<UniqueOutputIteratorT, detail::it_value_t<KeysInputIteratorT>>>;
static_assert(!::cuda::std::execution::__queryable_with<EnvT, ::cuda::execution::determinism::__get_determinism_t>,
"Determinism should be used inside requires to have an effect.");

using requirements_t = ::cuda::std::execution::
__query_result_or_t<EnvT, ::cuda::execution::__get_requirements_t, ::cuda::std::execution::env<>>;
using requested_determinism_t =
::cuda::std::execution::__query_result_or_t<requirements_t,
::cuda::execution::determinism::__get_determinism_t,
::cuda::execution::determinism::run_to_run_t>;
using accum_t = ::cuda::std::__accumulator_t<ReductionOpT, detail::it_value_t<ValuesInputIteratorT>>;
using key_t = detail::non_void_value_t<UniqueOutputIteratorT, detail::it_value_t<KeysInputIteratorT>>;
using OffsetT = detail::choose_offset_t<NumItemsT>;
using EqualityOp = ::cuda::std::equal_to<>;

constexpr bool is_run_to_run_required =
::cuda::std::is_same_v<requested_determinism_t, ::cuda::execution::determinism::run_to_run_t>;
constexpr bool is_gpu_to_gpu_required =
::cuda::std::is_same_v<requested_determinism_t, ::cuda::execution::determinism::gpu_to_gpu_t>;
constexpr bool is_safe_integral_op =
::cuda::std::is_integral_v<accum_t> && detail::is_cuda_binary_operator<ReductionOpT>;
constexpr bool is_primitive_min_max_op =
detail::is_primitive_v<accum_t> && detail::is_cuda_minimum_maximum_v<ReductionOpT, accum_t>;
constexpr bool is_fp_plus_op =
::cuda::is_floating_point_v<accum_t> && detail::is_cuda_std_plus_v<ReductionOpT, accum_t>;

static_assert(!is_run_to_run_required || is_safe_integral_op || is_primitive_min_max_op || is_fp_plus_op,
"run_to_run deterministic reduce-by-key requires integral types with known operators, "
"primitive types with min/max, or floating-point types with plus operator");
static_assert(!is_gpu_to_gpu_required || is_safe_integral_op || is_primitive_min_max_op,
"gpu_to_gpu deterministic reduce-by-key requires integral types with known operators or "
"primitive types with min/max");

static constexpr bool stable_reduction_order = is_run_to_run_required && is_fp_plus_op;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static constexpr bool stable_reduction_order = is_run_to_run_required && is_fp_plus_op;
constexpr bool stable_reduction_order = is_run_to_run_required && is_fp_plus_op;

No need to make it static (and below as well)

using default_policy_selector = detail::reduce_by_key::policy_selector_from_types<ReductionOpT, accum_t, key_t>;
return detail::dispatch_with_env_and_tuning<default_policy_selector>(
env, [&](auto policy_selector, void* storage, size_t& bytes, cudaStream_t stream) {
return detail::reduce_by_key::dispatch(
return detail::reduce_by_key::dispatch<stable_reduction_order>(
storage,
bytes,
d_keys_in,
Expand Down Expand Up @@ -2867,8 +2910,7 @@ public:
//! respectively. The total number of runs encountered is written to ``d_num_runs_out``.
//!
//! - The ``==`` equality operator is used to determine whether keys are equivalent
//! - Provides "run-to-run" determinism for pseudo-associative reduction
//! (e.g., addition of floating point types) on the same GPU device.
//! - Provides "run-to-run" determinism for floating-point reduction with ``cuda::std::plus`` on the same GPU device.
//! However, results for pseudo-associative reduction may be inconsistent
//! from one device to a another device of a different compute-capability
//! because CUB can employ different tile-sizing for different architectures.
Expand Down Expand Up @@ -3011,10 +3053,14 @@ public:
{
_CCCL_NVTX_RANGE_SCOPE_IF(d_temp_storage, "cub::DeviceReduce::ReduceByKey");

using accum_t = ::cuda::std::__accumulator_t<ReductionOpT, detail::it_value_t<ValuesInputIteratorT>>;
using OffsetT = detail::choose_offset_t<NumItemsT>;
using EqualityOp = ::cuda::std::equal_to<>;

return detail::reduce_by_key::dispatch(
static constexpr bool stable_reduction_order =
::cuda::is_floating_point_v<accum_t> && detail::is_cuda_std_plus_v<ReductionOpT, accum_t>;

return detail::reduce_by_key::dispatch<stable_reduction_order>(
d_temp_storage,
temp_storage_bytes,
d_keys_in,
Expand Down
36 changes: 26 additions & 10 deletions cub/cub/device/dispatch/dispatch_reduce_by_key.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ struct streaming_context
}
};

// Bind the stability flag while selecting the default and fallback agent policies.
template <bool StableReductionOrder, typename Policy, typename... Args>
using vsmem_helper_t =
vsmem_helper_with_fallback_impl<Policy,
AgentReduceByKey<Policy, StableReductionOrder, Args...>,
policy_wrapper_t<Policy, 64, 1>,
AgentReduceByKey<policy_wrapper_t<Policy, 64, 1>, StableReductionOrder, Args...>>;

/**
* @brief Multi-block reduce-by-key sweep kernel entry point
*
Expand Down Expand Up @@ -145,6 +153,9 @@ struct streaming_context
* @tparam OffsetT
* Signed integer type for global offsets
*
* @tparam StableReductionOrder
* Whether to use a stable reduction order across tiles
*
* @param d_keys_in
* Pointer to the input sequence of keys
*
Expand Down Expand Up @@ -187,7 +198,8 @@ template <typename PolicySelector,
typename ReductionOpT,
typename OffsetT,
typename AccumT,
typename StreamingContextT>
typename StreamingContextT,
bool StableReductionOrder>
#if _CCCL_HAS_CONCEPTS()
requires reduce_by_key_policy_selector<PolicySelector>
#endif
Expand Down Expand Up @@ -217,9 +229,9 @@ __launch_bounds__(int(current_policy<PolicySelector>().lookback.threads_per_bloc
policy.lookback.lookback_delay.delay,
policy.lookback.lookback_delay.l2_write_latency>>;

using vsmem_helper_t = vsmem_helper_default_fallback_policy_t<
using vsmem_helper_t = reduce_by_key::vsmem_helper_t<
StableReductionOrder,
AgentReduceByKeyPolicyT,
AgentReduceByKey,
KeysInputIteratorT,
UniqueOutputIteratorT,
ValuesInputIteratorT,
Expand Down Expand Up @@ -374,9 +386,9 @@ struct CCCL_DEPRECATED_BECAUSE("Use the tuning API for DeviceReduce::ReduceByKey
CUB_RUNTIME_FUNCTION _CCCL_VISIBILITY_HIDDEN _CCCL_FORCEINLINE cudaError_t
Invoke(ScanInitKernelT init_kernel, ReduceByKeyKernelT reduce_by_key_kernel)
{
using vsmem_helper_t = detail::vsmem_helper_default_fallback_policy_t<
using vsmem_helper_t = detail::reduce_by_key::vsmem_helper_t<
false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: why are we passing false here and not StableReductionOrder?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the deprecated dispatcher, which has no StableReductionOrder parameter, false preserves its existing ordering, the modern dispatcher forwards the flag

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahmedtaha100 please use an inline comment to indicate the name of the template parameter /*StableReductionOrder=*/false instead of a bare literal

typename ActivePolicyT::ReduceByKeyPolicyT,
detail::reduce_by_key::AgentReduceByKey,
KeysInputIteratorT,
UniqueOutputIteratorT,
ValuesInputIteratorT,
Expand Down Expand Up @@ -555,7 +567,8 @@ struct CCCL_DEPRECATED_BECAUSE("Use the tuning API for DeviceReduce::ReduceByKey
ReductionOpT,
OffsetT,
AccumT,
streaming_context_t>);
streaming_context_t,
false>);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

}

/**
Expand Down Expand Up @@ -650,7 +663,7 @@ struct CCCL_DEPRECATED_BECAUSE("Use the tuning API for DeviceReduce::ReduceByKey
namespace detail::reduce_by_key
{
// we move the conversion of the policy to the agent policy and its use out of the lambda below, so MSVC does not ICE
template <typename PolicyGetter, typename... Args>
template <bool StableReductionOrder, typename PolicyGetter, typename... Args>
_CCCL_HOST_DEVICE_API auto determine_threads_items_vsmem(PolicyGetter policy_getter)
{
// TODO(bgruber): refactor this in the future
Expand All @@ -664,13 +677,14 @@ _CCCL_HOST_DEVICE_API auto determine_threads_items_vsmem(PolicyGetter policy_get
delay_constructor_t<policy.lookback.lookback_delay.kind,
policy.lookback.lookback_delay.delay,
policy.lookback.lookback_delay.l2_write_latency>>;
using vsmem_helper_t = vsmem_helper_default_fallback_policy_t<Policy, AgentReduceByKey, Args...>;
using vsmem_helper_t = reduce_by_key::vsmem_helper_t<StableReductionOrder, Policy, Args...>;
return ::cuda::std::tuple{vsmem_helper_t::agent_policy_t::BLOCK_THREADS,
vsmem_helper_t::agent_policy_t::ITEMS_PER_THREAD,
vsmem_helper_t::vsmem_per_block};
}

template <typename KeysInputIteratorT,
template <bool StableReductionOrder,
typename KeysInputIteratorT,
typename UniqueOutputIteratorT,
typename ValuesInputIteratorT,
typename AggregatesOutputIteratorT,
Expand Down Expand Up @@ -721,6 +735,7 @@ CUB_RUNTIME_FUNCTION _CCCL_FORCEINLINE static cudaError_t dispatch(
#endif // _CCCL_HOSTED() && defined(CUB_DEBUG_LOG)

const auto [threads_per_block, items_per_thread, vsmem_per_block] = determine_threads_items_vsmem<
StableReductionOrder,
decltype(policy_getter),
KeysInputIteratorT,
UniqueOutputIteratorT,
Expand Down Expand Up @@ -799,7 +814,8 @@ CUB_RUNTIME_FUNCTION _CCCL_FORCEINLINE static cudaError_t dispatch(
ReductionOpT,
OffsetT,
AccumT,
streaming_context_t>;
streaming_context_t,
StableReductionOrder>;

int reduce_by_key_sm_occupancy{};
if (const auto error =
Expand Down
4 changes: 2 additions & 2 deletions cub/cub/device/dispatch/dispatch_rle_encode.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ __launch_bounds__(int(current_policy<PolicySelector>().lookback.threads_per_bloc
policy.lookback.lookback_delay.delay,
policy.lookback.lookback_delay.l2_write_latency>>;

using vsmem_helper_t = vsmem_helper_default_fallback_policy_t<
using vsmem_helper_t = reduce_by_key::vsmem_helper_t<
false,
AgentReduceByKeyPolicyT,
reduce_by_key::AgentReduceByKey,
KeysInputIteratorT,
UniqueOutputIteratorT,
ValuesInputIteratorT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct reduce_by_key_kernel_source
template <typename PolicySelector, typename... KernelArgTs>
[[nodiscard]] CUB_RUNTIME_FUNCTION static constexpr auto reduce_by_key_kernel()
{
return &DeviceReduceByKeyKernel<PolicySelector, KernelArgTs...>;
return &DeviceReduceByKeyKernel<PolicySelector, KernelArgTs..., false>;
}
};

Expand Down
89 changes: 89 additions & 0 deletions cub/test/catch2_test_device_reduce_by_key.cu
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

#include <cub/device/device_reduce.cuh>

#include <cuda/std/__bit/bit_cast.h>
#include <cuda/std/__functional/operations.h>

#include "catch2_test_device_reduce.cuh"
#include "catch2_test_launch_helper.h"
#include "cub_test_macros.h"
Expand Down Expand Up @@ -156,3 +159,89 @@ CUB_TEST("Device reduce-by-key works", "[by_key][reduce][device]", CUB_SMALL, fu
REQUIRE(expected_keys == out_unique_keys);
}
}

#if TEST_LAUNCH == 0 && TEST_TYPES == 0
CUB_TEST_CASE("Device reduce-by-key is run-to-run deterministic for fp64 sums", "[by_key][reduce][device]", CUB_SMALL)
{
// Span several waves of tiles so that the unfixed lookback grouping can vary on large GPUs.
constexpr int num_items = 262'144;
constexpr int long_run = 200'000;
constexpr int short_run = 64;
constexpr int num_repetitions = 20;
constexpr int num_runs = 1 + (num_items - long_run + short_run - 1) / short_run;

c2h::host_vector<std::uint64_t> keys(num_items);

for (int i = 0; i < num_items; ++i)
{
keys[i] = i < long_run ? 0 : 1 + (i - long_run) / short_run;
}

c2h::device_vector<std::uint64_t> keys_in = keys;
c2h::device_vector<double> values_in(num_items);
c2h::gen(C2H_SEED(2), values_in, -1e-2, 1e-2);
c2h::device_vector<std::uint64_t> unique_out(num_runs);
c2h::device_vector<double> aggregates_out(num_runs);
c2h::device_vector<int> num_runs_out(1);

const auto d_keys_in = thrust::raw_pointer_cast(keys_in.data());
const auto d_values_in = thrust::raw_pointer_cast(values_in.data());
const auto d_unique_out = thrust::raw_pointer_cast(unique_out.data());
const auto d_aggregates_out = thrust::raw_pointer_cast(aggregates_out.data());
const auto d_num_runs_out = thrust::raw_pointer_cast(num_runs_out.data());

std::size_t temp_storage_bytes{};
REQUIRE(
cudaSuccess
== cub::DeviceReduce::ReduceByKey(
nullptr,
temp_storage_bytes,
d_keys_in,
d_unique_out,
d_values_in,
d_aggregates_out,
d_num_runs_out,
cuda::std::plus{},
num_items));
c2h::device_vector<std::uint8_t> temp_storage(temp_storage_bytes, thrust::no_init);
const auto d_temp_storage = thrust::raw_pointer_cast(temp_storage.data());

c2h::host_vector<std::uint64_t> expected_keys;
c2h::host_vector<std::uint64_t> expected_aggregates;
for (int repetition = 0; repetition < num_repetitions; ++repetition)
{
REQUIRE(cudaSuccess == cudaMemset(d_aggregates_out, 0xee, num_runs * sizeof(double)));
REQUIRE(
cudaSuccess
== cub::DeviceReduce::ReduceByKey(
d_temp_storage,
temp_storage_bytes,
d_keys_in,
d_unique_out,
d_values_in,
d_aggregates_out,
d_num_runs_out,
cuda::std::plus{},
num_items));

REQUIRE(num_runs_out[0] == num_runs);
const c2h::host_vector<std::uint64_t> result_keys = unique_out;
const c2h::host_vector<double> result_aggregates = aggregates_out;
c2h::host_vector<std::uint64_t> result_bits(num_runs);
for (int run = 0; run < num_runs; ++run)
{
result_bits[run] = cuda::std::bit_cast<std::uint64_t>(result_aggregates[run]);
}
if (repetition == 0)
{
expected_keys = result_keys;
expected_aggregates = result_bits;
}
else
{
REQUIRE(result_keys == expected_keys);
REQUIRE(result_bits == expected_aggregates);
}
}
}
#endif // TEST_LAUNCH == 0 && TEST_TYPES == 0
Loading
Loading