| // 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(); |