| // 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 |
| |
| // Elementwise expressions through the Tensor executor: out = a + b, out = a * b + c and out = exp(a) over 2^16 to |
| // 2^26 elements, for float and half on aligned maps (the packet path) and unaligned ones (the scalar path), and for |
| // bfloat16, which has no device packet and takes the scalar path whatever the map. Baselines: a hand-written |
| // grid-stride float4 kernel and cudaMemcpyAsync device to device, which bounds what a memory-bound kernel can reach. |
| // Bytes per launch count every operand read once and the result written once. |
| |
| #include "gpu_bench_common.h" |
| |
| namespace { |
| |
| using eigen_bench::DeviceBuffer; |
| |
| const int kLaunchesPerIteration = 4; |
| const int kIterations = 10; |
| const std::vector<int64_t> kSizes = {1 << 16, 1 << 20, 1 << 24, 1 << 26}; |
| const double kExactTolerance = 0.0; |
| // Result checks in units of the type's epsilon: the device may fuse a * b + c (one rounding fewer than the host's |
| // two), and its exp is within a few ulps of the host's before the rounding to a narrow type. |
| template <typename T> |
| double epsilons(int count) { |
| return count * static_cast<double>(std::numeric_limits<T>::epsilon()); |
| } |
| |
| template <typename T, int MapOptions> |
| using Map1D = Eigen::TensorMap<Eigen::Tensor<T, 1>, MapOptions>; |
| |
| // One elementwise benchmark: NumInputs random device buffers, `expression(device, out, inputs)` enqueued on the |
| // executor, `reference(host_inputs, i)` checked once outside the timed loop, then the timing. |
| template <typename T, int MapOptions, int NumInputs, typename Expression, typename Reference> |
| void bench_elementwise(benchmark::State& state, Expression expression, Reference reference, double tolerance, |
| const char* name) { |
| const int64_t n = state.range(0); |
| std::vector<DeviceBuffer<T>> inputs; |
| std::vector<Map1D<T, MapOptions>> maps; |
| inputs.reserve(NumInputs); |
| for (int k = 0; k < NumInputs; ++k) { |
| inputs.emplace_back(n); |
| inputs.back().fillRandom(); |
| maps.push_back(Map1D<T, MapOptions>(inputs.back().data(), n)); |
| } |
| DeviceBuffer<T> out(n); |
| Eigen::GpuStreamDevice stream; |
| Eigen::GpuDevice device(&stream); |
| Map1D<T, MapOptions> tout(out.data(), n); |
| const auto enqueue = [&] { expression(device, tout, maps); }; |
| eigen_bench::warmUp(device.stream(), enqueue); |
| { |
| std::vector<std::vector<T>> host; |
| for (const auto& input : inputs) host.push_back(input.toHost()); |
| std::vector<T> expected(n); |
| for (int64_t i = 0; i < n; ++i) expected[i] = reference(host, i); |
| eigen_bench::requireClose(out.toHost(), expected, tolerance, name); |
| } |
| eigen_bench::timeLaunches(state, device.stream(), enqueue, kLaunchesPerIteration); |
| state.counters["bytes_per_second"] = eigen_bench::bytesPerSecond((NumInputs + 1.0) * n * sizeof(T)); |
| } |
| |
| template <typename T, int MapOptions> |
| void BM_Add(benchmark::State& state) { |
| using Map = Map1D<T, MapOptions>; |
| bench_elementwise<T, MapOptions, 2>( |
| state, |
| [](const Eigen::GpuDevice& device, Map& out, const std::vector<Map>& in) { out.device(device) = in[0] + in[1]; }, |
| [](const std::vector<std::vector<T>>& x, int64_t i) { return static_cast<T>(x[0][i] + x[1][i]); }, |
| kExactTolerance, "add"); |
| } |
| |
| template <typename T, int MapOptions> |
| void BM_MulAdd(benchmark::State& state) { |
| using Map = Map1D<T, MapOptions>; |
| bench_elementwise<T, MapOptions, 3>( |
| state, |
| [](const Eigen::GpuDevice& device, Map& out, const std::vector<Map>& in) { |
| out.device(device) = in[0] * in[1] + in[2]; |
| }, |
| [](const std::vector<std::vector<T>>& x, int64_t i) { return static_cast<T>(x[0][i] * x[1][i] + x[2][i]); }, |
| epsilons<T>(4), "mul_add"); |
| } |
| |
| template <typename T, int MapOptions> |
| void BM_Exp(benchmark::State& state) { |
| using Map = Map1D<T, MapOptions>; |
| bench_elementwise<T, MapOptions, 1>( |
| state, |
| [](const Eigen::GpuDevice& device, Map& out, const std::vector<Map>& in) { out.device(device) = in[0].exp(); }, |
| [](const std::vector<std::vector<T>>& x, int64_t i) { |
| return static_cast<T>(std::exp(static_cast<float>(x[0][i]))); |
| }, |
| epsilons<T>(8), "exp"); |
| } |
| |
| // What a kernel written by hand for this one expression reaches: 16-byte loads and stores, a grid sized to keep |
| // every multiprocessor busy, no evaluator. |
| __global__ void add_float4_kernel(const float4* a, const float4* b, float4* out, int n4) { |
| for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n4; i += gridDim.x * blockDim.x) { |
| const float4 x = a[i]; |
| const float4 y = b[i]; |
| out[i] = make_float4(x.x + y.x, x.y + y.y, x.z + y.z, x.w + y.w); |
| } |
| } |
| |
| void BM_AddHandWritten(benchmark::State& state) { |
| const int64_t n = state.range(0); |
| DeviceBuffer<float> a(n), b(n), out(n); |
| a.fillRandom(); |
| b.fillRandom(); |
| int device_id = 0; |
| EIGEN_GPU_RUNTIME_CHECK(cudaGetDevice(&device_id)); |
| int multiprocessors = 0; |
| EIGEN_GPU_RUNTIME_CHECK(cudaDeviceGetAttribute(&multiprocessors, cudaDevAttrMultiProcessorCount, device_id)); |
| const int n4 = static_cast<int>(n / 4); |
| const int block = 256; |
| const int grid = std::min((n4 + block - 1) / block, multiprocessors * 8); |
| cudaStream_t stream = nullptr; |
| const auto enqueue = [&] { |
| add_float4_kernel<<<grid, block, 0, stream>>>(reinterpret_cast<const float4*>(a.data()), |
| reinterpret_cast<const float4*>(b.data()), |
| reinterpret_cast<float4*>(out.data()), n4); |
| EIGEN_GPU_RUNTIME_CHECK(cudaGetLastError()); |
| }; |
| eigen_bench::warmUp(stream, enqueue); |
| { |
| const std::vector<float> ha = a.toHost(), hb = b.toHost(); |
| std::vector<float> expected(n); |
| for (int64_t i = 0; i < n; ++i) expected[i] = ha[i] + hb[i]; |
| eigen_bench::requireClose(out.toHost(), expected, kExactTolerance, "add_hand_written"); |
| } |
| eigen_bench::timeLaunches(state, stream, enqueue, kLaunchesPerIteration); |
| state.counters["bytes_per_second"] = eigen_bench::bytesPerSecond(3.0 * n * sizeof(float)); |
| } |
| |
| // The copy roofline: one read and one write per element, no arithmetic. |
| void BM_D2DMemcpy(benchmark::State& state) { |
| const int64_t n = state.range(0); |
| DeviceBuffer<float> a(n), out(n); |
| a.fillRandom(); |
| cudaStream_t stream = nullptr; |
| const auto enqueue = [&] { |
| EIGEN_GPU_RUNTIME_CHECK(cudaMemcpyAsync(out.data(), a.data(), n * sizeof(float), cudaMemcpyDeviceToDevice, stream)); |
| }; |
| eigen_bench::warmUp(stream, enqueue); |
| eigen_bench::timeLaunches(state, stream, enqueue, kLaunchesPerIteration); |
| state.counters["bytes_per_second"] = eigen_bench::bytesPerSecond(2.0 * n * sizeof(float)); |
| } |
| |
| } // namespace |
| |
| // bfloat16 has no device packet, so it is registered once, as the scalar path it is. |
| // bfloat16 has no device packet, so it is registered once, as the scalar path it is. |
| #define EIGEN_GPU_ELEMENTWISE_BENCHMARKS(NAME) \ |
| EIGEN_GPU_BENCHMARK_SIZES(NAME, float, Eigen::Aligned); \ |
| EIGEN_GPU_BENCHMARK_SIZES(NAME, float, Eigen::Unaligned); \ |
| EIGEN_GPU_BENCHMARK_SIZES(NAME, Eigen::half, Eigen::Aligned); \ |
| EIGEN_GPU_BENCHMARK_SIZES(NAME, Eigen::half, Eigen::Unaligned); \ |
| EIGEN_GPU_BENCHMARK_SIZES(NAME, Eigen::bfloat16, Eigen::Unaligned); |
| |
| EIGEN_GPU_ELEMENTWISE_BENCHMARKS(BM_Add) |
| EIGEN_GPU_ELEMENTWISE_BENCHMARKS(BM_MulAdd) |
| EIGEN_GPU_ELEMENTWISE_BENCHMARKS(BM_Exp) |
| EIGEN_GPU_BENCHMARK(BM_AddHandWritten)->ArgsProduct({kSizes}); |
| EIGEN_GPU_BENCHMARK(BM_D2DMemcpy)->ArgsProduct({kSizes}); |
| |
| EIGEN_GPU_BENCHMARK_MAIN() |