blob: 1b1535480f28d55fc21df20b2a4951ee5e377602 [file] [edit]
// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// Copyright (C) 2014 Benoit Steiner <benoit.steiner.goog@gmail.com>
//
// 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-License-Identifier: MPL-2.0
#include "main.h"
#include <limits>
#include <numeric>
#include <Eigen/Tensor>
using Eigen::Tensor;
template <int DataLayout>
static void test_trivial_reductions() {
{
Tensor<float, 0, DataLayout> tensor;
tensor.setRandom();
array<ptrdiff_t, 0> reduction_axis;
Tensor<float, 0, DataLayout> result = tensor.sum(reduction_axis);
VERIFY_IS_EQUAL(result(), tensor());
}
{
Tensor<float, 1, DataLayout> tensor(7);
tensor.setRandom();
array<ptrdiff_t, 0> reduction_axis;
Tensor<float, 1, DataLayout> result = tensor.sum(reduction_axis);
VERIFY_IS_EQUAL(result.dimension(0), 7);
for (int i = 0; i < 7; ++i) {
VERIFY_IS_EQUAL(result(i), tensor(i));
}
}
{
Tensor<float, 2, DataLayout> tensor(2, 3);
tensor.setRandom();
array<ptrdiff_t, 0> reduction_axis;
Tensor<float, 2, DataLayout> result = tensor.sum(reduction_axis);
VERIFY_IS_EQUAL(result.dimension(0), 2);
VERIFY_IS_EQUAL(result.dimension(1), 3);
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
VERIFY_IS_EQUAL(result(i, j), tensor(i, j));
}
}
}
}
template <typename Scalar, int DataLayout>
static void test_scalar_reduction_conversion() {
using TensorType = Tensor<Scalar, 4, DataLayout>;
using PartialReductionDims = array<ptrdiff_t, 2>;
using FullReduction = decltype(std::declval<const TensorType&>().sum());
using PartialReduction = decltype(std::declval<const TensorType&>().sum(std::declval<const PartialReductionDims&>()));
static_assert(std::is_convertible<FullReduction, Scalar>::value, "full reductions should convert to scalars");
static_assert(!std::is_convertible<PartialReduction, Scalar>::value,
"partial reductions should not convert to scalars");
TensorType tensor(2, 2, 2, 2);
Scalar expected_sum(0);
Scalar expected_prod(1);
Scalar expected_min = NumTraits<Scalar>::highest();
Scalar expected_max = NumTraits<Scalar>::lowest();
for (int i = 0; i < tensor.size(); ++i) {
// Bound the product so the half test does not overflow.
const Scalar value = static_cast<Scalar>((i % 3) + 1);
tensor(i) = value;
expected_sum += value;
expected_prod *= value;
expected_min = numext::mini(expected_min, value);
expected_max = numext::maxi(expected_max, value);
}
const Scalar sum = tensor.sum();
array<ptrdiff_t, 4> reduction_axis4 = {0, 1, 2, 3};
const Scalar sum_from_dims = tensor.sum(reduction_axis4);
const Scalar prod = tensor.prod();
const Scalar smallest = tensor.minimum();
const Scalar largest = tensor.maximum();
const Scalar mean = tensor.mean();
const Scalar expected_mean = expected_sum / static_cast<Scalar>(tensor.size());
if (NumTraits<Scalar>::IsInteger) {
VERIFY_IS_EQUAL(sum, expected_sum);
VERIFY_IS_EQUAL(sum_from_dims, expected_sum);
VERIFY_IS_EQUAL(prod, expected_prod);
VERIFY_IS_EQUAL(smallest, expected_min);
VERIFY_IS_EQUAL(largest, expected_max);
VERIFY_IS_EQUAL(mean, expected_mean);
} else {
VERIFY_IS_APPROX(sum, expected_sum);
VERIFY_IS_APPROX(sum_from_dims, expected_sum);
VERIFY_IS_APPROX(prod, expected_prod);
VERIFY_IS_APPROX(smallest, expected_min);
VERIFY_IS_APPROX(largest, expected_max);
VERIFY_IS_APPROX(mean, expected_mean);
}
}
template <typename Scalar, int DataLayout>
static void test_scalar_reduction_mixed_operators() {
// Mixed scalar operations must stay lazy despite the reduction's implicit scalar conversion.
Tensor<Scalar, 2, DataLayout> tensor(3, 4);
tensor.setConstant(Scalar(2));
// Scalar-left overloads must not pollute ADL for NumTraits enums.
static_assert(NumTraits<Scalar>::MulCost + 1 > 0, "NumTraits enum arithmetic should not be ambiguous");
static_assert(!std::is_same<decltype(tensor.sum() > 0), bool>::value,
"scalar comparisons on a full reduction should stay lazy expressions");
static_assert(!std::is_arithmetic<decltype(2 * tensor.sum())>::value,
"scalar arithmetic on a full reduction should stay lazy expressions");
const Tensor<bool, 0, DataLayout> gt = tensor.sum() > 0;
VERIFY(gt());
const Tensor<bool, 0, DataLayout> eq = tensor.sum() == 24;
VERIFY(eq());
const Tensor<bool, 0, DataLayout> lt = tensor.sum() < 1L;
VERIFY(!lt());
const Tensor<bool, 0, DataLayout> ge = tensor.sum() >= 0.0f;
VERIFY(ge());
const Tensor<bool, 0, DataLayout> le = tensor.sum() <= 100u;
VERIFY(le());
const Tensor<bool, 0, DataLayout> ne = tensor.sum() != 0;
VERIFY(ne());
const Tensor<Scalar, 0, DataLayout> twice = 2 * tensor.sum();
VERIFY_IS_EQUAL(twice(), Scalar(48));
const Tensor<Scalar, 0, DataLayout> plus_one = 1 + tensor.sum();
VERIFY_IS_EQUAL(plus_one(), Scalar(25));
const Tensor<Scalar, 0, DataLayout> from_25 = 25 - tensor.sum();
VERIFY_IS_EQUAL(from_25(), Scalar(1));
const Tensor<Scalar, 0, DataLayout> ratio = 48 / tensor.sum();
VERIFY_IS_EQUAL(ratio(), Scalar(2));
const Tensor<Scalar, 0, DataLayout> scaled = 2.0 * tensor.sum();
VERIFY_IS_EQUAL(scaled(), Scalar(48));
// Unscoped enums must remain valid scalar operands.
enum MixedOpsTestEnum { kEnumTwo = 2 };
const Tensor<bool, 0, DataLayout> gt_enum = tensor.sum() > kEnumTwo;
VERIFY(gt_enum());
const Tensor<Scalar, 0, DataLayout> twice_enum = kEnumTwo * tensor.sum();
VERIFY_IS_EQUAL(twice_enum(), Scalar(48));
}
static void test_scalar_reduction_mixed_operators_mod() {
Tensor<int, 2> tensor(3, 4);
tensor.setConstant(2);
const Tensor<int, 0> rem = tensor.sum() % 5L;
VERIFY_IS_EQUAL(rem(), 4);
}
template <typename Scalar, int DataLayout>
static void test_simple_reductions() {
Tensor<Scalar, 4, DataLayout> tensor(2, 3, 5, 7);
tensor.setRandom();
// Add a little offset so that the product reductions won't be close to zero.
tensor += tensor.constant(Scalar(0.5f));
array<ptrdiff_t, 2> reduction_axis2;
reduction_axis2[0] = 1;
reduction_axis2[1] = 3;
Tensor<Scalar, 2, DataLayout> result = tensor.sum(reduction_axis2);
VERIFY_IS_EQUAL(result.dimension(0), 2);
VERIFY_IS_EQUAL(result.dimension(1), 5);
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 5; ++j) {
Scalar sum = Scalar(0.0f);
for (int k = 0; k < 3; ++k) {
for (int l = 0; l < 7; ++l) {
sum += tensor(i, k, j, l);
}
}
VERIFY_IS_APPROX(result(i, j), sum);
}
}
{
Tensor<Scalar, 0, DataLayout> sum1 = tensor.sum();
VERIFY_IS_EQUAL(sum1.rank(), 0);
array<ptrdiff_t, 4> reduction_axis4;
reduction_axis4[0] = 0;
reduction_axis4[1] = 1;
reduction_axis4[2] = 2;
reduction_axis4[3] = 3;
Tensor<Scalar, 0, DataLayout> sum2 = tensor.sum(reduction_axis4);
VERIFY_IS_EQUAL(sum2.rank(), 0);
VERIFY_IS_APPROX(sum1(), sum2());
}
reduction_axis2[0] = 0;
reduction_axis2[1] = 2;
result = tensor.prod(reduction_axis2);
VERIFY_IS_EQUAL(result.dimension(0), 3);
VERIFY_IS_EQUAL(result.dimension(1), 7);
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 7; ++j) {
Scalar prod = Scalar(1.0f);
for (int k = 0; k < 2; ++k) {
for (int l = 0; l < 5; ++l) {
prod *= tensor(k, i, l, j);
}
}
VERIFY_IS_APPROX(result(i, j), prod);
}
}
{
Tensor<Scalar, 0, DataLayout> prod1 = tensor.prod();
VERIFY_IS_EQUAL(prod1.rank(), 0);
array<ptrdiff_t, 4> reduction_axis4;
reduction_axis4[0] = 0;
reduction_axis4[1] = 1;
reduction_axis4[2] = 2;
reduction_axis4[3] = 3;
Tensor<Scalar, 0, DataLayout> prod2 = tensor.prod(reduction_axis4);
VERIFY_IS_EQUAL(prod2.rank(), 0);
VERIFY_IS_APPROX(prod1(), prod2());
}
reduction_axis2[0] = 0;
reduction_axis2[1] = 2;
result = tensor.maximum(reduction_axis2);
VERIFY_IS_EQUAL(result.dimension(0), 3);
VERIFY_IS_EQUAL(result.dimension(1), 7);
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 7; ++j) {
Scalar max_val = std::numeric_limits<Scalar>::lowest();
for (int k = 0; k < 2; ++k) {
for (int l = 0; l < 5; ++l) {
max_val = (std::max)(max_val, tensor(k, i, l, j));
}
}
VERIFY_IS_APPROX(result(i, j), max_val);
}
}
{
Tensor<Scalar, 0, DataLayout> max1 = tensor.maximum();
VERIFY_IS_EQUAL(max1.rank(), 0);
array<ptrdiff_t, 4> reduction_axis4;
reduction_axis4[0] = 0;
reduction_axis4[1] = 1;
reduction_axis4[2] = 2;
reduction_axis4[3] = 3;
Tensor<Scalar, 0, DataLayout> max2 = tensor.maximum(reduction_axis4);
VERIFY_IS_EQUAL(max2.rank(), 0);
VERIFY_IS_APPROX(max1(), max2());
}
reduction_axis2[0] = 0;
reduction_axis2[1] = 1;
result = tensor.minimum(reduction_axis2);
VERIFY_IS_EQUAL(result.dimension(0), 5);
VERIFY_IS_EQUAL(result.dimension(1), 7);
for (int i = 0; i < 5; ++i) {
for (int j = 0; j < 7; ++j) {
Scalar min_val = (std::numeric_limits<Scalar>::max)();
for (int k = 0; k < 2; ++k) {
for (int l = 0; l < 3; ++l) {
min_val = (std::min)(min_val, tensor(k, l, i, j));
}
}
VERIFY_IS_APPROX(result(i, j), min_val);
}
}
{
Tensor<Scalar, 0, DataLayout> min1 = tensor.minimum();
VERIFY_IS_EQUAL(min1.rank(), 0);
array<ptrdiff_t, 4> reduction_axis4;
reduction_axis4[0] = 0;
reduction_axis4[1] = 1;
reduction_axis4[2] = 2;
reduction_axis4[3] = 3;
Tensor<Scalar, 0, DataLayout> min2 = tensor.minimum(reduction_axis4);
VERIFY_IS_EQUAL(min2.rank(), 0);
VERIFY_IS_APPROX(min1(), min2());
}
reduction_axis2[0] = 0;
reduction_axis2[1] = 1;
result = tensor.mean(reduction_axis2);
VERIFY_IS_EQUAL(result.dimension(0), 5);
VERIFY_IS_EQUAL(result.dimension(1), 7);
for (int i = 0; i < 5; ++i) {
for (int j = 0; j < 7; ++j) {
Scalar sum = Scalar(0.0f);
int count = 0;
for (int k = 0; k < 2; ++k) {
for (int l = 0; l < 3; ++l) {
sum += tensor(k, l, i, j);
++count;
}
}
VERIFY_IS_APPROX(result(i, j), sum / Scalar(count));
}
}
{
Tensor<Scalar, 0, DataLayout> mean1 = tensor.mean();
VERIFY_IS_EQUAL(mean1.rank(), 0);
array<ptrdiff_t, 4> reduction_axis4;
reduction_axis4[0] = 0;
reduction_axis4[1] = 1;
reduction_axis4[2] = 2;
reduction_axis4[3] = 3;
Tensor<Scalar, 0, DataLayout> mean2 = tensor.mean(reduction_axis4);
VERIFY_IS_EQUAL(mean2.rank(), 0);
VERIFY_IS_APPROX(mean1(), mean2());
}
{
Tensor<int, 1> ints(10);
std::iota(ints.data(), ints.data() + ints.dimension(0), 0);
TensorFixedSize<bool, Sizes<>> all_;
all_ = ints.all();
VERIFY(!all_());
all_ = (ints >= ints.constant(0)).all();
VERIFY(all_());
TensorFixedSize<bool, Sizes<>> any;
any = (ints > ints.constant(10)).any();
VERIFY(!any());
any = (ints < ints.constant(1)).any();
VERIFY(any());
}
}
template <int DataLayout>
static void test_reductions_in_expr() {
Tensor<float, 4, DataLayout> tensor(2, 3, 5, 7);
tensor.setRandom();
array<ptrdiff_t, 2> reduction_axis2;
reduction_axis2[0] = 1;
reduction_axis2[1] = 3;
Tensor<float, 2, DataLayout> result(2, 5);
result = result.constant(1.0f) - tensor.sum(reduction_axis2);
VERIFY_IS_EQUAL(result.dimension(0), 2);
VERIFY_IS_EQUAL(result.dimension(1), 5);
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 5; ++j) {
float sum = 0.0f;
for (int k = 0; k < 3; ++k) {
for (int l = 0; l < 7; ++l) {
sum += tensor(i, k, j, l);
}
}
VERIFY_IS_APPROX(result(i, j), 1.0f - sum);
}
}
}
template <int DataLayout>
static void test_full_reductions() {
Tensor<float, 2, DataLayout> tensor(2, 3);
tensor.setRandom();
array<ptrdiff_t, 2> reduction_axis;
reduction_axis[0] = 0;
reduction_axis[1] = 1;
Tensor<float, 0, DataLayout> result = tensor.sum(reduction_axis);
VERIFY_IS_EQUAL(result.rank(), 0);
float sum = 0.0f;
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
sum += tensor(i, j);
}
}
VERIFY_IS_APPROX(result(0), sum);
result = tensor.square().sum(reduction_axis).sqrt();
VERIFY_IS_EQUAL(result.rank(), 0);
sum = 0.0f;
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
sum += tensor(i, j) * tensor(i, j);
}
}
VERIFY_IS_APPROX(result(), sqrtf(sum));
}
struct UserReducer {
static const bool PacketAccess = false;
UserReducer(float offset) : offset_(offset) {}
void reduce(const float val, float* accum) { *accum += val * val; }
float initialize() const { return 0; }
float finalize(const float accum) const { return 1.0f / (accum + offset_); }
private:
const float offset_;
};
// A reducer that transforms each value it accepts (sum of squares), with reducer_traits
// declaring PacketAccess. Its reduce() cannot merge two partial accumulators — feeding a partial
// sum back through reduce() would square it — so the scalar unrolled path must not engage: the
// gate is reducer_can_reorder_accumulators, which such a reducer does not opt into, not
// PacketAccess. long double has no packets, which forces the scalar path while the declared
// PacketAccess stays true.
struct SquaredSumReducer {
EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void reduce(const long double t, long double* accum) const { *accum += t * t; }
EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE long double initialize() const { return 0; }
EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE long double finalize(const long double accum) const { return accum; }
};
namespace Eigen {
namespace internal {
template <typename Device>
struct reducer_traits<SquaredSumReducer, Device> {
enum { Cost = 1, PacketAccess = true, IsStateful = false, IsExactlyAssociative = true };
};
} // namespace internal
} // namespace Eigen
template <int DataLayout>
static void test_value_transforming_reducer() {
// The opt-ins that let the unrolled path interleave operands, pinned at both ends: a reducer
// that transforms each value, and one that carries state, must stay out however they declare
// PacketAccess.
STATIC_CHECK((internal::reducer_can_reorder_accumulators<internal::SumReducer<float>>::value));
STATIC_CHECK((internal::reducer_can_reorder_accumulators<internal::MinReducer<float>>::value));
STATIC_CHECK((!internal::reducer_can_reorder_accumulators<SquaredSumReducer>::value));
STATIC_CHECK((!internal::reducer_can_reorder_accumulators<internal::MeanReducer<float>>::value));
Tensor<long double, 1, DataLayout> t(12);
long double expected = 0;
for (int i = 0; i < 12; ++i) {
t(i) = static_cast<long double>(i + 1);
expected += t(i) * t(i);
}
array<ptrdiff_t, 1> reduction_axis;
reduction_axis[0] = 0;
SquaredSumReducer reducer;
Tensor<long double, 0, DataLayout> result = t.reduce(reduction_axis, reducer);
VERIFY_IS_EQUAL(result(), expected); // 1^2 + ... + 12^2 = 650, exact in long double
}
template <int DataLayout>
static void test_user_defined_reductions() {
Tensor<float, 2, DataLayout> tensor(5, 7);
tensor.setRandom();
array<ptrdiff_t, 1> reduction_axis;
reduction_axis[0] = 1;
UserReducer reducer(10.0f);
Tensor<float, 1, DataLayout> result = tensor.reduce(reduction_axis, reducer);
VERIFY_IS_EQUAL(result.dimension(0), 5);
for (int i = 0; i < 5; ++i) {
float expected = 10.0f;
for (int j = 0; j < 7; ++j) {
expected += tensor(i, j) * tensor(i, j);
}
expected = 1.0f / expected;
VERIFY_IS_APPROX(result(i), expected);
}
}
template <int DataLayout>
static void test_tensor_maps() {
int inputs[2 * 3 * 5 * 7];
TensorMap<Tensor<int, 4, DataLayout>> tensor_map(inputs, 2, 3, 5, 7);
TensorMap<Tensor<const int, 4, DataLayout>> tensor_map_const(inputs, 2, 3, 5, 7);
const TensorMap<Tensor<const int, 4, DataLayout>> tensor_map_const_const(inputs, 2, 3, 5, 7);
setRandomDataInRange(tensor_map, -1000, 1000);
array<ptrdiff_t, 2> reduction_axis;
reduction_axis[0] = 1;
reduction_axis[1] = 3;
Tensor<int, 2, DataLayout> result = tensor_map.sum(reduction_axis);
Tensor<int, 2, DataLayout> result2 = tensor_map_const.sum(reduction_axis);
Tensor<int, 2, DataLayout> result3 = tensor_map_const_const.sum(reduction_axis);
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 5; ++j) {
int sum = 0;
for (int k = 0; k < 3; ++k) {
for (int l = 0; l < 7; ++l) {
sum += tensor_map(i, k, j, l);
}
}
VERIFY_IS_EQUAL(result(i, j), sum);
VERIFY_IS_EQUAL(result2(i, j), sum);
VERIFY_IS_EQUAL(result3(i, j), sum);
}
}
}
template <int DataLayout>
static void test_static_dims() {
Tensor<float, 4, DataLayout> in(72, 53, 97, 113);
Tensor<float, 2, DataLayout> out(72, 97);
in.setRandom();
Eigen::IndexList<Eigen::type2index<1>, Eigen::type2index<3>> reduction_axis;
out = in.maximum(reduction_axis);
for (int i = 0; i < 72; ++i) {
for (int j = 0; j < 97; ++j) {
float expected = -1e10f;
for (int k = 0; k < 53; ++k) {
for (int l = 0; l < 113; ++l) {
expected = (std::max)(expected, in(i, k, j, l));
}
}
VERIFY_IS_EQUAL(out(i, j), expected);
}
}
}
template <int DataLayout>
static void test_innermost_last_dims() {
Tensor<float, 4, DataLayout> in(72, 53, 97, 113);
Tensor<float, 2, DataLayout> out(97, 113);
in.setRandom();
// Reduce on the innermost dimensions.
// This triggers the use of packets for ColMajor.
Eigen::IndexList<Eigen::type2index<0>, Eigen::type2index<1>> reduction_axis;
out = in.maximum(reduction_axis);
for (int i = 0; i < 97; ++i) {
for (int j = 0; j < 113; ++j) {
float expected = -1e10f;
for (int k = 0; k < 53; ++k) {
for (int l = 0; l < 72; ++l) {
expected = (std::max)(expected, in(l, k, i, j));
}
}
VERIFY_IS_EQUAL(out(i, j), expected);
}
}
}
template <int DataLayout>
static void test_innermost_first_dims() {
Tensor<float, 4, DataLayout> in(72, 53, 97, 113);
Tensor<float, 2, DataLayout> out(72, 53);
in.setRandom();
// Reduce on the innermost dimensions.
// This triggers the use of packets for RowMajor.
Eigen::IndexList<Eigen::type2index<2>, Eigen::type2index<3>> reduction_axis;
out = in.maximum(reduction_axis);
for (int i = 0; i < 72; ++i) {
for (int j = 0; j < 53; ++j) {
float expected = -1e10f;
for (int k = 0; k < 97; ++k) {
for (int l = 0; l < 113; ++l) {
expected = (std::max)(expected, in(i, j, k, l));
}
}
VERIFY_IS_EQUAL(out(i, j), expected);
}
}
}
template <int DataLayout>
static void test_reduce_middle_dims() {
Tensor<float, 4, DataLayout> in(72, 53, 97, 113);
Tensor<float, 2, DataLayout> out(72, 53);
in.setRandom();
// Reduce on the innermost dimensions.
// This triggers the use of packets for RowMajor.
Eigen::IndexList<Eigen::type2index<1>, Eigen::type2index<2>> reduction_axis;
out = in.maximum(reduction_axis);
for (int i = 0; i < 72; ++i) {
for (int j = 0; j < 113; ++j) {
float expected = -1e10f;
for (int k = 0; k < 53; ++k) {
for (int l = 0; l < 97; ++l) {
expected = (std::max)(expected, in(i, k, l, j));
}
}
VERIFY_IS_EQUAL(out(i, j), expected);
}
}
}
// Exercises the runtime innermost-dim detection added at TensorReduction.h:817
// (coeff path) and :861 (packet path). The static `ReducingInnerMostDims`
// predicate only fires when the reduce-dim container is an IndexList; with
// Eigen::array it stays false at compile time, but the m_reducingInnerMostDims
// runtime mirror picks up the same fast path. We verify correctness against a
// manual sum and check that the result is bit-identical to the IndexList form.
template <int DataLayout>
static void test_dynamic_innermost_dims() {
// 2D: ColMajor reduces axis 0 (innermost); RowMajor reduces axis 1 (innermost).
// Size chosen >> PacketSize so packet() drives the hot path.
Tensor<float, 2, DataLayout> in_2d(127, 53);
in_2d.setRandom();
Eigen::array<int, 1> dyn_axis_2d{(DataLayout == ColMajor) ? 0 : 1};
Tensor<float, 1, DataLayout> out_2d_dyn = in_2d.sum(dyn_axis_2d);
Eigen::IndexList<Eigen::type2index<(DataLayout == ColMajor) ? 0 : 1>> static_axis_2d;
Tensor<float, 1, DataLayout> out_2d_static = in_2d.sum(static_axis_2d);
const int preserved_2d = (DataLayout == ColMajor) ? 53 : 127;
const int reduced_2d = (DataLayout == ColMajor) ? 127 : 53;
for (int i = 0; i < preserved_2d; ++i) {
float expected = 0;
for (int j = 0; j < reduced_2d; ++j) {
expected += (DataLayout == ColMajor) ? in_2d(j, i) : in_2d(i, j);
}
VERIFY_IS_APPROX(out_2d_dyn(i), expected);
VERIFY_IS_EQUAL(out_2d_dyn(i), out_2d_static(i));
}
// 4D: reduce the two innermost dims at once. This is the multi-reduce case
// where the runtime mirror also needs to recognize the contiguous block.
Tensor<float, 4, DataLayout> in_4d(31, 17, 19, 23);
in_4d.setRandom();
Eigen::array<int, 2> dyn_axes_4d = (DataLayout == ColMajor) ? Eigen::array<int, 2>{0, 1} : Eigen::array<int, 2>{2, 3};
Tensor<float, 2, DataLayout> out_4d_dyn = in_4d.sum(dyn_axes_4d);
Eigen::IndexList<Eigen::type2index<(DataLayout == ColMajor) ? 0 : 2>,
Eigen::type2index<(DataLayout == ColMajor) ? 1 : 3>>
static_axes_4d;
Tensor<float, 2, DataLayout> out_4d_static = in_4d.sum(static_axes_4d);
if (DataLayout == ColMajor) {
for (int i = 0; i < 19; ++i) {
for (int j = 0; j < 23; ++j) {
VERIFY_IS_EQUAL(out_4d_dyn(i, j), out_4d_static(i, j));
}
}
} else {
for (int i = 0; i < 31; ++i) {
for (int j = 0; j < 17; ++j) {
VERIFY_IS_EQUAL(out_4d_dyn(i, j), out_4d_static(i, j));
}
}
}
// Negative case: reducing a non-innermost dim must NOT take the runtime
// fast path. We can't observe the dispatch directly, but bit-identity with
// a manual sum + agreement with the IndexList form covers correctness.
Eigen::array<int, 1> outer_axis{(DataLayout == ColMajor) ? 1 : 0};
Tensor<float, 1, DataLayout> out_outer = in_2d.sum(outer_axis);
const int preserved_outer = (DataLayout == ColMajor) ? 127 : 53;
const int reduced_outer = (DataLayout == ColMajor) ? 53 : 127;
for (int i = 0; i < preserved_outer; ++i) {
float expected = 0;
for (int j = 0; j < reduced_outer; ++j) {
expected += (DataLayout == ColMajor) ? in_2d(i, j) : in_2d(j, i);
}
VERIFY_IS_APPROX(out_outer(i), expected);
}
}
template <int DataLayout>
static void test_mixed_type_scalar_arithmetic() {
Tensor<float, 2, DataLayout> in(2, 2);
in.setConstant(1.0f);
Tensor<float, 0, DataLayout> result;
// Mixed type: double OP TensorReductionOp<float>
result = 0.5 * in.sum();
VERIFY_IS_EQUAL(result(), 2.0f);
result = 0.5 + in.sum();
VERIFY_IS_EQUAL(result(), 4.5f);
result = 0.5 - in.sum();
VERIFY_IS_EQUAL(result(), -3.5f);
result = 2.0 / in.sum();
VERIFY_IS_EQUAL(result(), 0.5f);
// Mixed type: TensorReductionOp<float> OP double
result = in.sum() * 0.5;
VERIFY_IS_EQUAL(result(), 2.0f);
result = in.sum() + 0.5;
VERIFY_IS_EQUAL(result(), 4.5f);
result = in.sum() - 0.5;
VERIFY_IS_EQUAL(result(), 3.5f);
result = in.sum() / 0.5;
VERIFY_IS_EQUAL(result(), 8.0f);
// Mixed type: TensorReductionOp<float> OP int
result = in.sum() * 2;
VERIFY_IS_EQUAL(result(), 8.0f);
// Reductions on both sides must keep using the tensor-tensor operators rather than the
// scalar overloads reachable through the rank-0 conversion to Scalar.
result = in.sum() * in.sum();
VERIFY_IS_EQUAL(result(), 16.0f);
result = in.sum() - in.maximum();
VERIFY_IS_EQUAL(result(), 3.0f);
}
template <typename ScalarType, int num_elements, int max_mean>
void test_sum_accuracy() {
Tensor<double, 1> double_tensor(num_elements);
Tensor<ScalarType, 1> tensor(num_elements);
for (double prescribed_mean = 0; prescribed_mean <= max_mean;
prescribed_mean = numext::maxi(1.0, prescribed_mean * 3.99)) {
// FIXME: NormalRandomGenerator doesn't work in bfloat and half.
double_tensor.setRandom<Eigen::internal::NormalRandomGenerator<double>>();
double_tensor += double_tensor.constant(prescribed_mean);
tensor = double_tensor.cast<ScalarType>();
Tensor<ScalarType, 0> sum;
sum = tensor.sum();
// Compute the reference value in double precision.
double expected_sum = 0.0;
double abs_sum = 0.0;
for (int i = 0; i < num_elements; ++i) {
expected_sum += static_cast<double>(tensor(i));
abs_sum += static_cast<double>(numext::abs(tensor(i)));
}
// Test against probabilistic forward error bound. In reality, the error is much smaller
// when we use tree summation.
double err = Eigen::numext::abs(static_cast<double>(sum()) - expected_sum);
double tol = numext::sqrt(static_cast<double>(num_elements)) *
static_cast<double>(NumTraits<ScalarType>::epsilon()) * abs_sum;
VERIFY_LE(err, tol);
}
}
EIGEN_DECLARE_TEST(tensor_reduction) {
CALL_SUBTEST(test_trivial_reductions<ColMajor>());
CALL_SUBTEST(test_trivial_reductions<RowMajor>());
CALL_SUBTEST((test_scalar_reduction_conversion<int, ColMajor>()));
CALL_SUBTEST((test_scalar_reduction_conversion<int, RowMajor>()));
CALL_SUBTEST((test_scalar_reduction_conversion<Eigen::half, ColMajor>()));
CALL_SUBTEST((test_scalar_reduction_mixed_operators<float, ColMajor>()));
CALL_SUBTEST((test_scalar_reduction_mixed_operators<double, ColMajor>()));
CALL_SUBTEST((test_scalar_reduction_mixed_operators<int, RowMajor>()));
CALL_SUBTEST(test_scalar_reduction_mixed_operators_mod());
CALL_SUBTEST((test_simple_reductions<float, ColMajor>()));
CALL_SUBTEST((test_simple_reductions<float, RowMajor>()));
CALL_SUBTEST((test_simple_reductions<Eigen::half, ColMajor>()));
CALL_SUBTEST((test_simple_reductions<Eigen::bfloat16, ColMajor>()));
CALL_SUBTEST(test_reductions_in_expr<ColMajor>());
CALL_SUBTEST(test_reductions_in_expr<RowMajor>());
CALL_SUBTEST(test_full_reductions<ColMajor>());
CALL_SUBTEST(test_full_reductions<RowMajor>());
CALL_SUBTEST(test_user_defined_reductions<ColMajor>());
CALL_SUBTEST(test_user_defined_reductions<RowMajor>());
CALL_SUBTEST(test_value_transforming_reducer<ColMajor>());
CALL_SUBTEST(test_value_transforming_reducer<RowMajor>());
CALL_SUBTEST(test_tensor_maps<ColMajor>());
CALL_SUBTEST(test_tensor_maps<RowMajor>());
CALL_SUBTEST(test_static_dims<ColMajor>());
CALL_SUBTEST(test_static_dims<RowMajor>());
CALL_SUBTEST(test_innermost_last_dims<ColMajor>());
CALL_SUBTEST(test_innermost_last_dims<RowMajor>());
CALL_SUBTEST(test_innermost_first_dims<ColMajor>());
CALL_SUBTEST(test_innermost_first_dims<RowMajor>());
CALL_SUBTEST(test_reduce_middle_dims<ColMajor>());
CALL_SUBTEST(test_reduce_middle_dims<RowMajor>());
CALL_SUBTEST(test_dynamic_innermost_dims<ColMajor>());
CALL_SUBTEST(test_dynamic_innermost_dims<RowMajor>());
CALL_SUBTEST((test_sum_accuracy<float, 10 * 1024 * 1024, 8 * 1024>()));
CALL_SUBTEST((test_sum_accuracy<Eigen::bfloat16, 10 * 1024 * 1024, 8 * 1024>()));
// The range of half is limited to 65519 when using round-to-even,
// so we are severely limited in the size and mean of the tensors
// we can reduce without overflow.
CALL_SUBTEST((test_sum_accuracy<Eigen::half, 4 * 1024, 16>()));
CALL_SUBTEST((test_sum_accuracy<Eigen::half, 10 * 1024 * 1024, 0>()));
CALL_SUBTEST(test_mixed_type_scalar_arithmetic<ColMajor>());
CALL_SUBTEST(test_mixed_type_scalar_arithmetic<RowMajor>());
}