blob: eac1a71632d4633e160bfaa0e864b330431f03a2 [file] [edit]
// 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()