blob: f79258a57f64b4ae0e4de8289bcaa11a53a5bf10 [file]
// Benchmarks for Eigen Tensor broadcasting.
// Tests broadcasting along various dimensions and ranks.
// SPDX-FileCopyrightText: The Eigen Authors
// SPDX-License-Identifier: MPL-2.0
#define EIGEN_USE_THREADS
#include <benchmark/benchmark.h>
#include <contrib/Eigen/Tensor>
#include <Eigen/ThreadPool>
using namespace Eigen;
typedef float Scalar;
// --- Broadcast row vector {1,N} -> {M,N} ---
static void BM_BroadcastRow(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> row(1, N);
Tensor<Scalar, 2> result(M, N);
row.setRandom();
Eigen::array<int, 2> bcast = {M, 1};
for (auto _ : state) {
result = row.broadcast(bcast);
benchmark::DoNotOptimize(result.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * M * N * sizeof(Scalar));
}
// --- Broadcast col vector {M,1} -> {M,N} ---
static void BM_BroadcastCol(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> col(M, 1);
Tensor<Scalar, 2> result(M, N);
col.setRandom();
Eigen::array<int, 2> bcast = {1, N};
for (auto _ : state) {
result = col.broadcast(bcast);
benchmark::DoNotOptimize(result.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * M * N * sizeof(Scalar));
}
// --- Broadcast + element-wise add (bias addition pattern) ---
static void BM_BroadcastAdd(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> mat(M, N);
Tensor<Scalar, 2> bias(1, N);
Tensor<Scalar, 2> result(M, N);
mat.setRandom();
bias.setRandom();
Eigen::array<int, 2> bcast = {M, 1};
for (auto _ : state) {
result = mat + bias.broadcast(bcast);
benchmark::DoNotOptimize(result.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * M * N * sizeof(Scalar) * 2);
}
// --- Broadcast + scalar clamp (bias addition + clamp pattern) ---
// cwiseMax(Scalar) / cwiseMin(Scalar) build constant() leaves (unlike
// clip(), which is a single unary functor). Constants used to disable tiled
// evaluation for the whole expression.
static void BM_BroadcastClamp(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> mat(M, N);
Tensor<Scalar, 2> bias(1, N);
Tensor<Scalar, 2> result(M, N);
mat.setRandom();
bias.setRandom();
Eigen::array<int, 2> bcast = {M, 1};
for (auto _ : state) {
result = (mat + bias.broadcast(bcast)).cwiseMax(Scalar(-0.5)).cwiseMin(Scalar(0.5));
benchmark::DoNotOptimize(result.data());
benchmark::ClobberMemory();
}
// Validate outside the timed loop.
for (int i = 0; i < M; ++i) {
for (int j = 0; j < N; ++j) {
const Scalar expected = std::min(Scalar(0.5), std::max(Scalar(-0.5), mat(i, j) + bias(0, j)));
if (result(i, j) != expected) {
state.SkipWithError("validation failed");
return;
}
}
}
state.SetBytesProcessed(state.iterations() * M * N * sizeof(Scalar) * 2);
}
// --- Outer-dimension broadcast + clip ---
// Broadcasting a large plane along the outer-most dimension has a reuse
// distance of the whole plane: the untiled path streams the plane from
// memory B times, while tiled evaluation reuses cache-resident pieces
// across the broadcast factor. The cwiseMax/cwiseMin constants decide
// which of the two paths runs, so before nullary block support they forced
// the memory-bound path.
static void BM_OuterBroadcastClamp(benchmark::State& state) {
const int D0 = state.range(0);
const int D1 = state.range(1);
const int B = state.range(2);
Tensor<Scalar, 3> plane(D0, D1, 1);
Tensor<Scalar, 3> result(D0, D1, B);
plane.setRandom();
Eigen::array<int, 3> bcast = {1, 1, B};
for (auto _ : state) {
result = plane.broadcast(bcast).cwiseMax(Scalar(-0.5)).cwiseMin(Scalar(0.5));
benchmark::DoNotOptimize(result.data());
benchmark::ClobberMemory();
}
for (int i = 0; i < D0; ++i) {
for (int j = 0; j < D1; ++j) {
const Scalar expected = std::min(Scalar(0.5), std::max(Scalar(-0.5), plane(i, j, 0)));
for (int k = 0; k < B; ++k) {
if (result(i, j, k) != expected) {
state.SkipWithError("validation failed");
return;
}
}
}
}
state.SetBytesProcessed(state.iterations() * D0 * D1 * B * sizeof(Scalar));
}
// --- Transpose (shuffle) + scalar clamp ---
// A shuffled inner dimension makes the linear path a per-element gather with
// a full index-remapping walk, while the block path runs a cache-blocked
// transpose through TensorBlockIO. The cwiseMax/cwiseMin constants decide
// which path runs, so before nullary block support they forced the gather.
static void BM_ShuffleClamp(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> mat(M, N);
Tensor<Scalar, 2> result(N, M);
mat.setRandom();
Eigen::array<int, 2> transpose = {1, 0};
for (auto _ : state) {
result = mat.shuffle(transpose).cwiseMax(Scalar(-0.5)).cwiseMin(Scalar(0.5));
benchmark::DoNotOptimize(result.data());
benchmark::ClobberMemory();
}
for (int i = 0; i < N; ++i) {
for (int j = 0; j < M; ++j) {
const Scalar expected = std::min(Scalar(0.5), std::max(Scalar(-0.5), mat(j, i)));
if (result(i, j) != expected) {
state.SkipWithError("validation failed");
return;
}
}
}
state.SetBytesProcessed(state.iterations() * M * N * sizeof(Scalar));
}
// --- Pad + scalar clamp (convolution-style padding + clamp) ---
static void BM_PadClamp(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
const int P = state.range(2);
Tensor<Scalar, 2> mat(M, N);
Tensor<Scalar, 2> result(M + 2 * P, N + 2 * P);
mat.setRandom();
Eigen::array<std::pair<int, int>, 2> paddings;
paddings[0] = {P, P};
paddings[1] = {P, P};
for (auto _ : state) {
result = mat.pad(paddings).cwiseMax(Scalar(-0.5)).cwiseMin(Scalar(0.5));
benchmark::DoNotOptimize(result.data());
benchmark::ClobberMemory();
}
for (int i = 0; i < M + 2 * P; ++i) {
for (int j = 0; j < N + 2 * P; ++j) {
const bool interior = i >= P && i < M + P && j >= P && j < N + P;
const Scalar padded = interior ? mat(i - P, j - P) : Scalar(0);
const Scalar expected = std::min(Scalar(0.5), std::max(Scalar(-0.5), padded));
if (result(i, j) != expected) {
state.SkipWithError("validation failed");
return;
}
}
}
state.SetBytesProcessed(state.iterations() * (M + 2 * P) * (N + 2 * P) * sizeof(Scalar));
}
// --- Constant multiply without any block-preferring node (control) ---
// No node prefers block access here, so this expression stays on the linear
// vectorized path; it pins that nullary block support does not change it.
static void BM_ConstantMul(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> mat(M, N);
Tensor<Scalar, 2> result(M, N);
mat.setRandom();
for (auto _ : state) {
result = mat * mat.constant(Scalar(3));
benchmark::DoNotOptimize(result.data());
benchmark::ClobberMemory();
}
for (int i = 0; i < M; ++i) {
for (int j = 0; j < N; ++j) {
if (result(i, j) != Scalar(3) * mat(i, j)) {
state.SkipWithError("validation failed");
return;
}
}
}
state.SetBytesProcessed(state.iterations() * M * N * sizeof(Scalar) * 2);
}
// --- Rank-4 broadcast (batch x channels x 1 x 1) -> (batch x channels x H x W) ---
static void BM_BroadcastRank4(benchmark::State& state) {
const int batch = state.range(0);
const int C = state.range(1);
const int H = state.range(2);
Tensor<Scalar, 4> bias(batch, C, 1, 1);
Tensor<Scalar, 4> result(batch, C, H, H);
bias.setRandom();
Eigen::array<int, 4> bcast = {1, 1, H, H};
for (auto _ : state) {
result = bias.broadcast(bcast);
benchmark::DoNotOptimize(result.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * batch * C * H * H * sizeof(Scalar));
}
// --- ThreadPool variants ---
static void BM_BroadcastRow_ThreadPool(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
const int threads = state.range(2);
Tensor<Scalar, 2> row(1, N);
Tensor<Scalar, 2> result(M, N);
row.setRandom();
ThreadPool tp(threads);
ThreadPoolDevice dev(&tp, threads);
Eigen::array<int, 2> bcast = {M, 1};
for (auto _ : state) {
result.device(dev) = row.broadcast(bcast);
benchmark::DoNotOptimize(result.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * M * N * sizeof(Scalar));
state.counters["threads"] = threads;
}
static void BM_BroadcastAdd_ThreadPool(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
const int threads = state.range(2);
Tensor<Scalar, 2> mat(M, N);
Tensor<Scalar, 2> bias(1, N);
Tensor<Scalar, 2> result(M, N);
mat.setRandom();
bias.setRandom();
ThreadPool tp(threads);
ThreadPoolDevice dev(&tp, threads);
Eigen::array<int, 2> bcast = {M, 1};
for (auto _ : state) {
result.device(dev) = mat + bias.broadcast(bcast);
benchmark::DoNotOptimize(result.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * M * N * sizeof(Scalar) * 2);
state.counters["threads"] = threads;
}
// {m, n} and {batch, c, h}: pure Cartesian products.
#define BROADCAST_SIZES ->ArgsProduct({{64, 256, 1024}, {64, 256, 1024}})
#define BROADCAST_RANK4_SIZES ->ArgsProduct({{1, 8}, {64, 256}, {16, 32}})
// {size, size, threads}: explicit because size is repeated.
// clang-format off
#define BROADCAST_THREADPOOL_SIZES \
->Args({256, 256, 1})->Args({256, 256, 2})->Args({256, 256, 4}) \
->Args({256, 256, 8})->Args({256, 256, 12})->Args({256, 256, 16}) \
->Args({1024, 1024, 1})->Args({1024, 1024, 2})->Args({1024, 1024, 4}) \
->Args({1024, 1024, 8})->Args({1024, 1024, 12})->Args({1024, 1024, 16})
// clang-format on
BENCHMARK(BM_BroadcastRow) BROADCAST_SIZES;
BENCHMARK(BM_BroadcastCol) BROADCAST_SIZES;
BENCHMARK(BM_BroadcastAdd) BROADCAST_SIZES;
BENCHMARK(BM_BroadcastClamp) BROADCAST_SIZES;
BENCHMARK(BM_OuterBroadcastClamp)->ArgsProduct({{512, 1024}, {512, 1024}, {8}});
BENCHMARK(BM_ShuffleClamp)->ArgsProduct({{256, 1024, 4096}, {256, 1024, 4096}});
BENCHMARK(BM_PadClamp)->ArgsProduct({{256, 1024}, {256, 1024}, {1, 4}});
BENCHMARK(BM_ConstantMul) BROADCAST_SIZES;
BENCHMARK(BM_BroadcastRank4) BROADCAST_RANK4_SIZES;
BENCHMARK(BM_BroadcastRow_ThreadPool) BROADCAST_THREADPOOL_SIZES->UseRealTime();
BENCHMARK(BM_BroadcastAdd_ThreadPool) BROADCAST_THREADPOOL_SIZES->UseRealTime();