| // This file is part of Eigen, a lightweight C++ template library |
| // for linear algebra. |
| // |
| // This Source Code Form is subject to the terms of the Mozilla |
| // Public License v. 2.0. If a copy of the MPL was not distributed |
| // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| // SPDX-FileCopyrightText: The Eigen Authors |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| // Tensor reductions on GpuDevice against CUB, which is the reference implementation of these kernels: full sum and |
| // max over 2^20 to 2^26 elements, and the two partial reductions of a matrix, over the inner dimension (unit |
| // stride, the coalesced direction) and the outer one. Bytes per launch count the input read once. |
| |
| #include "gpu_bench_common.h" |
| |
| #include <cub/cub.cuh> |
| #include <iterator> |
| |
| namespace { |
| |
| using eigen_bench::DeviceBuffer; |
| |
| constexpr int kLaunchesPerIteration = 4; |
| constexpr int kIterations = 10; |
| constexpr int64_t kFullSizes[] = {1 << 20, 1 << 24, 1 << 26}; |
| // The three matrix shapes the partial reductions consume are registered as (rows, columns) pairs rather than as a |
| // product of two lists, whose cross product would ask for tensors of up to 2^40 elements: the square case, a wide |
| // one whose reduced extent is short, and a tall one whose reduced extent is long. |
| |
| template <typename T> |
| using Vec = Eigen::TensorMap<Eigen::Tensor<T, 1>, Eigen::Aligned>; |
| template <typename T> |
| using Mat = Eigen::TensorMap<Eigen::Tensor<T, 2>, Eigen::Aligned>; |
| template <typename T> |
| using Scalar0 = Eigen::TensorMap<Eigen::Tensor<T, 0>, Eigen::Aligned>; |
| |
| // Compare against a compensated Neumaier sum. For these random inputs, use an error scale of |
| // eps * log2(n) * ||x||_2 for trees and eps * sqrt(n) * ||x||_2 for sequential accumulation, with a safety factor. |
| // These are statistical tolerances for this benchmark's inputs, not worst-case bounds for arbitrary data. |
| enum Accumulation { kTree, kInOrder }; |
| |
| struct SumReference { |
| double value; |
| double tolerance; |
| }; |
| |
| template <typename T, typename Element> |
| SumReference referenceSum(Accumulation accumulation, int64_t count, Element element) { |
| double sum = 0.0; |
| double compensation = 0.0; |
| double sum_squares = 0.0; |
| for (int64_t i = 0; i < count; ++i) { |
| const double x = static_cast<double>(element(i)); |
| const double total = sum + x; |
| compensation += (std::fabs(sum) >= std::fabs(x)) ? (sum - total) + x : (x - total) + sum; |
| sum = total; |
| sum_squares += x * x; |
| } |
| const double depth = std::log2(static_cast<double>(count)) + 1.0; |
| const double growth = (accumulation == kTree) ? depth : std::fmax(depth, std::sqrt(static_cast<double>(count))); |
| const double epsilon = static_cast<double>(std::numeric_limits<T>::epsilon()); |
| return {sum + compensation, 16.0 * growth * epsilon * std::sqrt(sum_squares)}; |
| } |
| |
| void requireSum(double got, const SumReference& reference, const char* what, int64_t index) { |
| if (!(std::fabs(got - reference.value) <= reference.tolerance)) { |
| std::fprintf(stderr, "%s: output %lld is %g, expected %g (tolerance %g)\n", what, static_cast<long long>(index), |
| got, reference.value, reference.tolerance); |
| std::abort(); |
| } |
| } |
| |
| template <typename T> |
| void checkSum(const DeviceBuffer<T>& out, const std::vector<T>& input, int64_t count, const char* what) { |
| const SumReference reference = referenceSum<T>(kTree, count, [&](int64_t i) { return input[i]; }); |
| requireSum(static_cast<double>(out.toHost()[0]), reference, what, 0); |
| } |
| |
| // One reference per output: dimension 0 sums each column of the column-major matrix, dimension 1 each row. A |
| // benchmark that never reads its output cannot tell a fast kernel from a wrong one, so both partial reductions |
| // and the CUB baseline are checked once before the timed loop. |
| template <typename T, int Dim> |
| void checkPartialSums(const DeviceBuffer<T>& out, const std::vector<T>& input, int64_t rows, int64_t cols, |
| const char* what) { |
| const std::vector<T> got = out.toHost(); |
| const int64_t out_size = (Dim == 0) ? cols : rows; |
| const int64_t reduced = (Dim == 0) ? rows : cols; |
| for (int64_t o = 0; o < out_size; ++o) { |
| const SumReference reference = referenceSum<T>( |
| kInOrder, reduced, [&](int64_t i) { return (Dim == 0) ? input[i + o * rows] : input[o + i * rows]; }); |
| requireSum(static_cast<double>(got[o]), reference, what, o); |
| } |
| } |
| |
| template <typename T> |
| void BM_FullReduceSum(benchmark::State& state) { |
| const int64_t n = state.range(0); |
| DeviceBuffer<T> in(n), out(1); |
| in.fillRandom(); |
| Eigen::GpuStreamDevice stream; |
| Eigen::GpuDevice device(&stream); |
| Vec<T> tin(in.data(), n); |
| Scalar0<T> tout(out.data()); |
| const auto enqueue = [&] { tout.device(device) = tin.sum(); }; |
| eigen_bench::warmUp(device.stream(), enqueue); |
| checkSum(out, in.toHost(), n, "full_sum"); |
| eigen_bench::timeLaunches(state, device.stream(), enqueue, kLaunchesPerIteration); |
| state.counters["bytes_per_second"] = eigen_bench::bytesPerSecond(static_cast<double>(n) * sizeof(T)); |
| } |
| |
| template <typename T> |
| void BM_FullReduceMax(benchmark::State& state) { |
| const int64_t n = state.range(0); |
| DeviceBuffer<T> in(n), out(1); |
| in.fillRandom(); |
| Eigen::GpuStreamDevice stream; |
| Eigen::GpuDevice device(&stream); |
| Vec<T> tin(in.data(), n); |
| Scalar0<T> tout(out.data()); |
| const auto enqueue = [&] { tout.device(device) = tin.maximum(); }; |
| eigen_bench::warmUp(device.stream(), enqueue); |
| { |
| const std::vector<T> host = in.toHost(); |
| const T expected = *std::max_element(host.begin(), host.end()); |
| eigen_bench::requireClose(out.toHost(), std::vector<T>{expected}, 0.0, "full_max"); |
| } |
| eigen_bench::timeLaunches(state, device.stream(), enqueue, kLaunchesPerIteration); |
| state.counters["bytes_per_second"] = eigen_bench::bytesPerSecond(static_cast<double>(n) * sizeof(T)); |
| } |
| |
| // Dimension 0 of a column-major tensor is unit stride within a column; dimension 1 strides by the number of rows, |
| // which is the case the double specialization used to fall back to the generic evaluator for. |
| template <typename T, int Dim> |
| void bench_partial_reduce(benchmark::State& state) { |
| const int64_t rows = state.range(0), cols = state.range(1); |
| const int64_t out_size = (Dim == 0) ? cols : rows; |
| DeviceBuffer<T> in(rows * cols), out(out_size); |
| in.fillRandom(); |
| Eigen::GpuStreamDevice stream; |
| Eigen::GpuDevice device(&stream); |
| Mat<T> tin(in.data(), rows, cols); |
| Vec<T> tout(out.data(), out_size); |
| const Eigen::array<Eigen::Index, 1> dims{Dim}; |
| const auto enqueue = [&] { tout.device(device) = tin.sum(dims); }; |
| eigen_bench::warmUp(device.stream(), enqueue); |
| checkPartialSums<T, Dim>(out, in.toHost(), rows, cols, Dim == 0 ? "reduce_inner" : "reduce_outer"); |
| eigen_bench::timeLaunches(state, device.stream(), enqueue, kLaunchesPerIteration); |
| state.counters["bytes_per_second"] = eigen_bench::bytesPerSecond(static_cast<double>(rows * cols) * sizeof(T)); |
| } |
| |
| template <typename T> |
| void BM_ReduceInner(benchmark::State& state) { |
| bench_partial_reduce<T, 0>(state); |
| } |
| |
| template <typename T> |
| void BM_ReduceOuter(benchmark::State& state) { |
| bench_partial_reduce<T, 1>(state); |
| } |
| |
| // CUB's own kernels, the baseline these are measured against. |
| template <typename T> |
| void BM_CubFullReduceSum(benchmark::State& state) { |
| const int64_t n = state.range(0); |
| DeviceBuffer<T> in(n), out(1); |
| in.fillRandom(); |
| cudaStream_t stream = nullptr; |
| size_t temp_bytes = 0; |
| EIGEN_GPU_RUNTIME_CHECK( |
| cub::DeviceReduce::Sum(nullptr, temp_bytes, in.data(), out.data(), static_cast<int>(n), stream)); |
| DeviceBuffer<char> temp(temp_bytes); |
| const auto enqueue = [&] { |
| size_t bytes = temp_bytes; |
| EIGEN_GPU_RUNTIME_CHECK( |
| cub::DeviceReduce::Sum(temp.data(), bytes, in.data(), out.data(), static_cast<int>(n), stream)); |
| }; |
| eigen_bench::warmUp(stream, enqueue); |
| checkSum(out, in.toHost(), n, "cub_full_sum"); |
| eigen_bench::timeLaunches(state, stream, enqueue, kLaunchesPerIteration); |
| state.counters["bytes_per_second"] = eigen_bench::bytesPerSecond(static_cast<double>(n) * sizeof(T)); |
| } |
| |
| // One segment per column, which is what an inner reduction of a column-major matrix computes. |
| template <typename T> |
| void BM_CubSegmentedReduceSum(benchmark::State& state) { |
| const int64_t rows = state.range(0), cols = state.range(1); |
| DeviceBuffer<T> in(rows * cols), out(cols); |
| in.fillRandom(); |
| std::vector<int> offsets(cols + 1); |
| for (int64_t c = 0; c <= cols; ++c) offsets[c] = static_cast<int>(c * rows); |
| DeviceBuffer<int> d_offsets(offsets.size()); |
| EIGEN_GPU_RUNTIME_CHECK( |
| cudaMemcpy(d_offsets.data(), offsets.data(), offsets.size() * sizeof(int), cudaMemcpyHostToDevice)); |
| cudaStream_t stream = nullptr; |
| size_t temp_bytes = 0; |
| EIGEN_GPU_RUNTIME_CHECK(cub::DeviceSegmentedReduce::Sum(nullptr, temp_bytes, in.data(), out.data(), |
| static_cast<int>(cols), d_offsets.data(), |
| d_offsets.data() + 1, stream)); |
| DeviceBuffer<char> temp(temp_bytes); |
| const auto enqueue = [&] { |
| size_t bytes = temp_bytes; |
| EIGEN_GPU_RUNTIME_CHECK(cub::DeviceSegmentedReduce::Sum(temp.data(), bytes, in.data(), out.data(), |
| static_cast<int>(cols), d_offsets.data(), |
| d_offsets.data() + 1, stream)); |
| }; |
| eigen_bench::warmUp(stream, enqueue); |
| checkPartialSums<T, 0>(out, in.toHost(), rows, cols, "cub_segmented_sum"); |
| eigen_bench::timeLaunches(state, stream, enqueue, kLaunchesPerIteration); |
| state.counters["bytes_per_second"] = eigen_bench::bytesPerSecond(static_cast<double>(rows * cols) * sizeof(T)); |
| } |
| |
| } // namespace |
| |
| #define EIGEN_GPU_FULL_REDUCTION_BENCHMARKS(NAME) \ |
| BENCHMARK_TEMPLATE(NAME, float) \ |
| ->ArgsProduct({{std::begin(kFullSizes), std::end(kFullSizes)}}) \ |
| ->UseManualTime() \ |
| ->Iterations(kIterations); \ |
| BENCHMARK_TEMPLATE(NAME, double) \ |
| ->ArgsProduct({{std::begin(kFullSizes), std::end(kFullSizes)}}) \ |
| ->UseManualTime() \ |
| ->Iterations(kIterations); |
| |
| // Explicit shapes rather than a product: the product of the two ranges asks for tensors of up to 2^40 elements. |
| // Args is (rows, columns), so for the outer reduction it reads as (outputs, reduced extent) and the last three |
| // shapes straddle the band OuterReducer::run admits doubles to OuterReductionKernel in: a reduced extent below |
| // its floor, a shape inside the band, and an output count above its ceiling on a 36-multiprocessor device. For |
| // the inner reduction and the CUB baseline the two extents swap roles. |
| #define EIGEN_GPU_PARTIAL_REDUCTION_SHAPES(NAME, TYPE) \ |
| BENCHMARK_TEMPLATE(NAME, TYPE) \ |
| ->Args({1 << 10, 1 << 10}) \ |
| ->Args({1 << 4, 1 << 20}) \ |
| ->Args({1 << 20, 1 << 4}) \ |
| ->Args({1 << 10, 33}) \ |
| ->Args({1 << 9, 1 << 7}) \ |
| ->Args({1 << 13, 1 << 9}) \ |
| ->UseManualTime() \ |
| ->Iterations(kIterations); |
| |
| #define EIGEN_GPU_PARTIAL_REDUCTION_BENCHMARKS(NAME) \ |
| EIGEN_GPU_PARTIAL_REDUCTION_SHAPES(NAME, float) \ |
| EIGEN_GPU_PARTIAL_REDUCTION_SHAPES(NAME, double) |
| |
| EIGEN_GPU_FULL_REDUCTION_BENCHMARKS(BM_FullReduceSum) |
| EIGEN_GPU_FULL_REDUCTION_BENCHMARKS(BM_FullReduceMax) |
| EIGEN_GPU_FULL_REDUCTION_BENCHMARKS(BM_CubFullReduceSum) |
| EIGEN_GPU_PARTIAL_REDUCTION_BENCHMARKS(BM_ReduceInner) |
| EIGEN_GPU_PARTIAL_REDUCTION_BENCHMARKS(BM_ReduceOuter) |
| EIGEN_GPU_PARTIAL_REDUCTION_BENCHMARKS(BM_CubSegmentedReduceSum) |
| |
| EIGEN_GPU_BENCHMARK_MAIN() |