blob: 0a9facda0730c286bb7c7bc69b5fddde8dcea0ee [file]
// SPDX-FileCopyrightText: The Eigen Authors
// SPDX-License-Identifier: MPL-2.0
#define TEST_ENABLE_TEMPORARY_TRACKING
#define EIGEN_RUNTIME_NO_MALLOC
#define TEST_IGNORE_STACK_ALLOCATED_TEMPORARY
#include "main.h"
#include "product_test_helpers.h"
// GEMM operands of the form A.cast<T>() are packed straight from A, and half and bfloat16 GEMMs accumulate in float.
// Temporaries per cast operand: none, or one with EIGEN_USE_BLAS or on SME, which materialize the cast.
#if !defined(EIGEN_USE_BLAS) && !defined(EIGEN_VECTORIZE_SME)
constexpr int kCastTemporaries = 0;
#else
constexpr int kCastTemporaries = 1;
#endif
template <typename Src, int Order>
Matrix<Src, Dynamic, Dynamic, Order> random_small_integers(Index rows, Index cols, int bound) {
Matrix<Src, Dynamic, Dynamic, Order> m(rows, cols);
for (Index j = 0; j < cols; ++j)
for (Index i = 0; i < rows; ++i) m(i, j) = Src(internal::random<int>(-bound, bound));
return m;
}
// Integers in [-bound, bound], or for a floating-point Src, fractions there that a narrowing cast rounds.
template <typename Src, int Order>
Matrix<Src, Dynamic, Dynamic, Order> random_operand(Index rows, Index cols, int bound) {
if (NumTraits<Src>::IsInteger) return random_small_integers<Src, Order>(rows, cols, bound);
return Matrix<Src, Dynamic, Dynamic, Order>::Random(rows, cols) * Src(bound);
}
// The lazily cast operands pack to the same blocks as the materialized casts, so the products agree bit for bit.
template <typename Src, typename Dst, int LhsOrder, int RhsOrder, int DstOrder>
void lazy_cast_product(Index rows, Index cols, Index depth) {
using DstMat = Matrix<Dst, Dynamic, Dynamic, DstOrder>;
using PlainDst = Matrix<Dst, Dynamic, Dynamic>;
Matrix<Src, Dynamic, Dynamic, LhsOrder> a = random_operand<Src, LhsOrder>(rows + 3, depth + 2, 8);
Matrix<Src, Dynamic, Dynamic, RhsOrder> b = random_operand<Src, RhsOrder>(depth + 1, cols + 4, 8);
// Strided operands: blocks of larger matrices.
auto A = a.block(1, 2, rows, depth);
auto B = b.block(1, 3, depth, cols);
Matrix<Dst, Dynamic, Dynamic, LhsOrder> Ad = A.template cast<Dst>();
Matrix<Dst, Dynamic, Dynamic, RhsOrder> Bd = B.template cast<Dst>();
DstMat ref(rows, cols), dst(rows, cols);
ref.noalias() = Ad * Bd;
dst.setRandom();
VERIFY_EVALUATION_COUNT(dst.noalias() = A.template cast<Dst>() * B.template cast<Dst>(), 2 * kCastTemporaries);
VERIFY_IS_EQUAL(dst, ref);
DstMat ref_update = ref;
ref_update.noalias() += Ad * Bd;
VERIFY_EVALUATION_COUNT(dst.noalias() += A.template cast<Dst>() * B.template cast<Dst>(), 2 * kCastTemporaries);
VERIFY_IS_EQUAL(dst, ref_update);
ref_update.noalias() -= Ad * Bd;
VERIFY_EVALUATION_COUNT(dst.noalias() -= A.template cast<Dst>() * B.template cast<Dst>(), 2 * kCastTemporaries);
VERIFY_IS_EQUAL(dst, ref_update);
// One side already in the kernel's type.
ref.noalias() = Ad * Bd;
VERIFY_EVALUATION_COUNT(dst.noalias() = A.template cast<Dst>() * Bd, kCastTemporaries);
VERIFY_IS_EQUAL(dst, ref);
VERIFY_EVALUATION_COUNT(dst.noalias() = Ad * B.template cast<Dst>(), kCastTemporaries);
VERIFY_IS_EQUAL(dst, ref);
// A transposed operand below the cast.
DstMat dst_t(cols, rows), ref_t(cols, rows);
ref_t.noalias() = Bd.transpose() * Ad.transpose();
VERIFY_EVALUATION_COUNT(dst_t.noalias() = B.transpose().template cast<Dst>() * A.transpose().template cast<Dst>(),
2 * kCastTemporaries);
VERIFY_IS_EQUAL(dst_t, ref_t);
// A scalar factor below the cast is not hoisted out of it, and the operand is materialized as before.
ref.noalias() = (Src(2) * A).template cast<Dst>().eval() * Bd;
dst.noalias() = (Src(2) * A).template cast<Dst>() * B.template cast<Dst>();
VERIFY_IS_EQUAL(dst, ref);
// A scalar factor above the cast multiplies the result.
ref.noalias() = (Dst(3) * Ad) * Bd;
dst.noalias() = (Dst(3) * A.template cast<Dst>()) * B.template cast<Dst>();
VERIFY_IS_EQUAL(dst, ref);
// Not a GEMM: the GEMV fallback for a single result column, which sums in another order than the GEMV kernel.
PlainDst col(rows, 1);
col.noalias() = A.template cast<Dst>() * B.col(0).template cast<Dst>();
VERIFY_IS_APPROX(col, (Ad * Bd.col(0)).eval());
}
template <typename Src, typename Dst>
void lazy_cast_products() {
const Index rows = internal::random<Index>(20, 300);
const Index cols = internal::random<Index>(20, 300);
const Index depth = internal::random<Index>(20, 300);
CALL_SUBTEST((lazy_cast_product<Src, Dst, ColMajor, ColMajor, ColMajor>(rows, cols, depth)));
CALL_SUBTEST((lazy_cast_product<Src, Dst, RowMajor, ColMajor, ColMajor>(rows, cols, depth)));
CALL_SUBTEST((lazy_cast_product<Src, Dst, ColMajor, RowMajor, RowMajor>(rows, cols, depth)));
CALL_SUBTEST((lazy_cast_product<Src, Dst, RowMajor, RowMajor, RowMajor>(rows, cols, depth)));
// Sizes that are not a multiple of the packet or the register block.
CALL_SUBTEST((lazy_cast_product<Src, Dst, ColMajor, RowMajor, ColMajor>(17, 13, 9)));
CALL_SUBTEST((lazy_cast_product<Src, Dst, RowMajor, ColMajor, RowMajor>(31, 7, 23)));
}
// A half or bfloat16 GEMM sums in float and rounds each coefficient once.
template <typename T, int LhsOrder, int RhsOrder, int DstOrder>
void float_accumulation(Index rows, Index cols, Index depth) {
using Mat = Matrix<T, Dynamic, Dynamic, DstOrder>;
// |sum| <= 256 * depth < 2^24 stays an integer that float holds exactly, so the reference does not depend on the
// order. Its spread of about 16^2 / 3 * sqrt(depth) exceeds half's and bfloat16's exact integers, so a sum in T
// rounds.
Matrix<T, Dynamic, Dynamic, LhsOrder> A = random_small_integers<T, LhsOrder>(rows, depth, 16);
Matrix<T, Dynamic, Dynamic, RhsOrder> B = random_small_integers<T, RhsOrder>(depth, cols, 16);
MatrixXf exact = A.template cast<float>().eval() * B.template cast<float>().eval();
Mat C(rows, cols);
C.setConstant(T(1000));
C.noalias() = A * B;
VERIFY_IS_EQUAL(C, exact.cast<T>().eval());
C = A * B;
VERIFY_IS_EQUAL(C, exact.cast<T>().eval());
Mat C0 = random_small_integers<T, DstOrder>(rows, cols, 64);
C = C0;
C.noalias() += A * B;
VERIFY_IS_EQUAL(C, (C0.template cast<float>() + exact).template cast<T>().eval());
C = C0;
C.noalias() -= A * B;
VERIFY_IS_EQUAL(C, (C0.template cast<float>() - exact).template cast<T>().eval());
// Strided destination and operands.
Mat big = Mat::Constant(rows + 2, cols + 3, T(7));
big.block(1, 2, rows, cols).noalias() = A.leftCols(depth) * B.topRows(depth);
VERIFY_IS_EQUAL(big.block(1, 2, rows, cols), exact.cast<T>().eval());
VERIFY_IS_EQUAL(big.row(0), Mat::Constant(1, cols + 3, T(7)));
VERIFY_IS_EQUAL(big.col(0), Mat::Constant(rows + 2, 1, T(7)));
VERIFY_IS_EQUAL(big.rightCols(1), Mat::Constant(rows + 2, 1, T(7)));
VERIFY_IS_EQUAL(big.bottomRows(1), Mat::Constant(1, cols + 3, T(7)));
}
template <typename T>
void float_accumulations() {
const Index rows = internal::random<Index>(20, 200);
const Index cols = internal::random<Index>(20, 200);
const Index depth = internal::random<Index>(20, 600);
CALL_SUBTEST((float_accumulation<T, ColMajor, ColMajor, ColMajor>(rows, cols, depth)));
CALL_SUBTEST((float_accumulation<T, RowMajor, ColMajor, RowMajor>(rows, cols, depth)));
CALL_SUBTEST((float_accumulation<T, RowMajor, RowMajor, ColMajor>(rows, cols, depth)));
CALL_SUBTEST((float_accumulation<T, ColMajor, RowMajor, RowMajor>(13, 9, 31)));
}
template <typename T>
void float_accumulation_depth() {
// Summed in T, a run of ones stalls where T's spacing reaches 2: at 2048 for half and 256 for bfloat16.
const Index depth = std::is_same<T, half>::value ? 4096 : 1024;
using Mat = Matrix<T, Dynamic, Dynamic>;
Mat A = Mat::Ones(24, depth), B = Mat::Ones(depth, 21);
Mat C = A * B;
VERIFY_IS_EQUAL(C, Mat::Constant(24, 21, T(float(depth))));
// Nonnegative operands, where no cancellation hides the growth of an error summed in T: here it stays one
// rounding to T at any depth.
Mat X = Mat::Random(24, depth).cwiseAbs(), Y = Mat::Random(depth, 21).cwiseAbs();
C.noalias() = X * Y;
VERIFY(verifyProduct<float>(C.template cast<double>(),
(X.template cast<double>().eval() * Y.template cast<double>().eval()).eval(), X, Y, 1));
// The scalar factors combine in float: s * t = 2^-28 is below half's and above bfloat16's smallest subnormal, and
// depth * s * t = 2^-16 (half) or 2^-18 (bfloat16) is representable in both.
const T s = T(std::ldexp(1.0f, -14));
C.noalias() = (s * A) * (B * s);
VERIFY_IS_EQUAL(C, Mat::Constant(24, 21, T(std::ldexp(float(depth), -28))));
// Where s * s underflows in T, a product small enough for the coefficient-based path would evaluate 0 * inf there,
// so it takes the float GEMM as well.
for (Index n : {5, 24}) {
Mat P = Mat::Ones(n, n), Q = Mat::Ones(n, n), R(n, n);
P(0, 0) = NumTraits<T>::infinity();
R.noalias() = (s * P) * (Q * s);
VERIFY(R.row(0).array().isInf().all());
VERIFY_IS_EQUAL(R.bottomRows(n - 1), Mat::Constant(n - 1, n, T(std::ldexp(float(n), -28))));
R.setOnes();
R.noalias() += (s * P) * (Q * s);
VERIFY(R.row(0).array().isInf().all());
VERIFY_IS_EQUAL(R.bottomRows(n - 1), Mat::Constant(n - 1, n, T(1.0f + std::ldexp(float(n), -28))));
}
}
// A cast to T is materialized in T, as before, rather than packed from the source and summed in float: the product
// sees T's overflow, its rounding of the source, and inf * 0.
template <typename T, typename Src>
void cast_to_float_accumulated() {
using SrcMat = Matrix<Src, Dynamic, Dynamic>;
using Mat = Matrix<T, Dynamic, Dynamic>;
const Index n = 32;
SrcMat A = random_small_integers<Src, ColMajor>(n, n, 8), B = random_small_integers<Src, ColMajor>(n, n, 8);
// Out of half's range, and above bfloat16's spacing of 2^10 at 2^17.
A(0, 0) = Src(std::is_same<T, half>::value ? 70000 : 205001);
// 2051 rounds to 2052 in half and 2048 in bfloat16.
A(1, 0) = Src(2051);
B.row(0).setConstant(Src(3));
B(0, 1) = Src(0);
Mat ref = A.template cast<T>().eval() * B.template cast<T>().eval();
Mat C = A.template cast<T>() * B.template cast<T>();
// inf and NaN coefficients compare unequal, so compare their classification and the finite rest.
VERIFY((C.array().isNaN() == ref.array().isNaN()).all());
VERIFY((C.array().isInf() == ref.array().isInf()).all());
VERIFY_IS_EQUAL(C.array().isFinite().select(C, T(0)).eval(), ref.array().isFinite().select(ref, T(0)).eval());
VERIFY_IS_EQUAL(C.array().isInf().any(), (std::is_same<T, half>::value));
}
// The float workspace goes where the packed blocks do: a fixed-size product above the stack limit compiles and runs,
// and a preallocated product below it does not allocate.
template <typename T>
void float_accumulation_workspace() {
using Fixed = Matrix<T, 192, 192>;
std::unique_ptr<Fixed> A(new Fixed), B(new Fixed), C(new Fixed);
*A = random_small_integers<T, ColMajor>(192, 192, 4);
*B = random_small_integers<T, ColMajor>(192, 192, 4);
C->noalias() = *A * *B;
using Mat = Matrix<T, Dynamic, Dynamic>;
MatrixXf Af = A->template cast<float>(), Bf = B->template cast<float>();
VERIFY_IS_EQUAL(Mat(*C), (Af * Bf).template cast<T>().eval());
// With EIGEN_USE_BLAS or on SME the float product materializes both casts.
#if !defined(EIGEN_USE_BLAS) && !defined(EIGEN_VECTORIZE_SME)
Mat X = random_small_integers<T, ColMajor>(48, 40, 4), Y = random_small_integers<T, ColMajor>(40, 36, 4);
MatrixXf exact = X.template cast<float>() * Y.template cast<float>();
Mat Z(48, 36);
internal::set_is_malloc_allowed(false);
Z.noalias() = X * Y;
internal::set_is_malloc_allowed(true);
VERIFY_IS_EQUAL(Z, exact.cast<T>().eval());
MatrixXf twice = Z.template cast<float>() + exact;
internal::set_is_malloc_allowed(false);
Z.noalias() += X * Y;
internal::set_is_malloc_allowed(true);
VERIFY_IS_EQUAL(Z, twice.cast<T>().eval());
#endif
// A runtime vector result takes GEMV on T, which does not allocate for a row-major matrix by a column or a row by a
// column-major matrix. Entries in [-1, 1] at depth 40 keep every partial sum exact in T.
Matrix<T, Dynamic, Dynamic, RowMajor> R = random_small_integers<T, RowMajor>(48, 40, 1);
Mat v = random_small_integers<T, ColMajor>(40, 1, 1), w = random_small_integers<T, ColMajor>(1, 40, 1);
Mat V = random_small_integers<T, ColMajor>(40, 36, 1), y(48, 1), z(1, 36);
internal::set_is_malloc_allowed(false);
y.noalias() = R * v;
z.noalias() = w * V;
internal::set_is_malloc_allowed(true);
VERIFY_IS_EQUAL(y, (R.template cast<float>() * v.template cast<float>()).template cast<T>().eval());
VERIFY_IS_EQUAL(z, (w.template cast<float>() * V.template cast<float>()).template cast<T>().eval());
// An empty product neither allocates nor rewrites dst, which keeps a NaN payload that a round trip through float
// would quieten.
Mat E0(200, 0), E1(0, 200), W = Mat::Ones(200, 200);
W(0, 0) = numext::bit_cast<T>(std::is_same<T, half>::value ? uint16_t(0x7d01) : uint16_t(0x7fa1));
const Mat W0 = W;
internal::set_is_malloc_allowed(false);
W.noalias() += E0 * E1;
internal::set_is_malloc_allowed(true);
VERIFY_IS_EQUAL(numext::bit_cast<uint16_t>(W(0, 0)), numext::bit_cast<uint16_t>(W0(0, 0)));
VERIFY_IS_EQUAL(W.bottomRows(199), W0.bottomRows(199));
W.noalias() = E0 * E1;
VERIFY_IS_EQUAL(W, Mat::Zero(200, 200));
}
EIGEN_DECLARE_TEST(product_cast) {
using MatrixXhalf = Matrix<half, Dynamic, Dynamic>;
using internal::gemm_accumulates_in_float;
using internal::gemm_operand_traits;
#if !defined(EIGEN_USE_BLAS) && !defined(EIGEN_VECTORIZE_SME)
STATIC_CHECK((std::is_same<gemm_operand_traits<decltype(MatrixXhalf().cast<float>())>::StorageScalar, half>::value));
STATIC_CHECK((
std::is_same<gemm_operand_traits<decltype(MatrixXf().transpose().cast<double>())>::StorageScalar, float>::value));
#endif
STATIC_CHECK(
(std::is_same<gemm_operand_traits<decltype((MatrixXhalf() + MatrixXhalf()).cast<float>())>::StorageScalar,
float>::value));
STATIC_CHECK((std::is_same<gemm_operand_traits<decltype((half(2) * MatrixXhalf()).cast<float>())>::StorageScalar,
float>::value));
STATIC_CHECK((std::is_same<gemm_operand_traits<decltype(MatrixXf().cast<std::complex<float>>())>::StorageScalar,
std::complex<float>>::value));
STATIC_CHECK((std::is_same<gemm_operand_traits<decltype(MatrixXf().cast<half>())>::StorageScalar, half>::value));
STATIC_CHECK((gemm_accumulates_in_float<half, half>::value));
#if !(defined(EIGEN_USE_BLAS) && defined(EIGEN_USE_OPENBLAS_BFLOAT16) && EIGEN_USE_OPENBLAS_BFLOAT16)
STATIC_CHECK((gemm_accumulates_in_float<bfloat16, bfloat16>::value));
#endif
STATIC_CHECK((!gemm_accumulates_in_float<half, bfloat16>::value));
STATIC_CHECK((!gemm_accumulates_in_float<float, float>::value));
STATIC_CHECK((!gemm_accumulates_in_float<double, double>::value));
for (int i = 0; i < g_repeat; i++) {
CALL_SUBTEST_1((lazy_cast_products<half, float>()));
CALL_SUBTEST_2((lazy_cast_products<bfloat16, float>()));
CALL_SUBTEST_3((lazy_cast_products<float, double>()));
CALL_SUBTEST_3((lazy_cast_products<int, float>()));
CALL_SUBTEST_3((lazy_cast_products<double, float>()));
CALL_SUBTEST_4((lazy_cast_products<int, double>()));
CALL_SUBTEST_4((lazy_cast_products<half, double>()));
}
for (int i = 0; i < g_repeat; i++) {
CALL_SUBTEST_5(float_accumulations<half>());
CALL_SUBTEST_6(float_accumulations<bfloat16>());
}
CALL_SUBTEST_5(float_accumulation_depth<half>());
CALL_SUBTEST_6(float_accumulation_depth<bfloat16>());
CALL_SUBTEST_5((cast_to_float_accumulated<half, float>()));
CALL_SUBTEST_5((cast_to_float_accumulated<half, double>()));
CALL_SUBTEST_6((cast_to_float_accumulated<bfloat16, float>()));
CALL_SUBTEST_6((cast_to_float_accumulated<bfloat16, double>()));
CALL_SUBTEST_5(float_accumulation_workspace<half>());
CALL_SUBTEST_6(float_accumulation_workspace<bfloat16>());
}