blob: d82400ba3a77a063927f541cc56b758ca4c2eab6 [file]
// 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
// GEMM assignment, C = A * B, which kernels that can overwrite (gebp_kernel_overwrites) compute without zeroing C
// first. Every overwritten coefficient is alpha * acc + 0, so the result must be bitwise the one that zeroing C and
// accumulating gives; C starts out NaN, so a coefficient not written, or computed from C's old contents, fails the
// comparison. Small blocking budgets split the products over several depth, row and column blocks, of which only
// the first depth block may overwrite.
#define EIGEN_DEBUG_SMALL_PRODUCT_BLOCKS
#include "main.h"
#include "product_test_helpers.h"
#include <limits>
template <typename Scalar>
Scalar nan_value() {
return Scalar(std::numeric_limits<typename NumTraits<Scalar>::Real>::quiet_NaN());
}
// Bitwise equality of every coefficient, so that +0 and -0 differ.
template <typename Real>
bool bits_equal(const Real& x, const Real& y) {
using Bits = typename numext::get_integer_by_size<sizeof(Real)>::unsigned_type;
return numext::bit_cast<Bits>(x) == numext::bit_cast<Bits>(y);
}
template <typename Real>
bool bits_equal(const std::complex<Real>& x, const std::complex<Real>& y) {
return bits_equal(x.real(), y.real()) && bits_equal(x.imag(), y.imag());
}
template <typename A, typename B>
bool bitwise_equal(const DenseBase<A>& a, const DenseBase<B>& b) {
if (a.rows() != b.rows() || a.cols() != b.cols()) return false;
for (Index j = 0; j < a.cols(); ++j)
for (Index i = 0; i < a.rows(); ++i)
if (!bits_equal(a.derived().coeff(i, j), b.derived().coeff(i, j))) return false;
return true;
}
// dst = scale * a * b through assignment, from a NaN-filled destination, against the zero-then-accumulate result and
// the coefficient-based product. The two must be bitwise equal where the product runs GEMM. The coefficient-based
// product and the GEMV fallbacks compile `=` and `+=` separately, and those may round differently.
template <typename Dst, typename Lhs, typename Rhs, typename Scalar>
void check_assign(Dst& dst, const Lhs& a, const Rhs& b, const Scalar& scale) {
using ScaledLhs = internal::remove_all_t<decltype(scale * a)>;
using Impl = internal::generic_product_impl<ScaledLhs, Rhs, DenseShape, DenseShape, GemmProduct>;
const bool gemm = dst.rows() > 1 && dst.cols() > 1 && !Impl::useRuntimeCoeffBasedProduct(dst, b);
// Accumulated into dst itself, so that it takes the same kernel (or external BLAS) as the assignment: a row-major or
// strided destination may take another, which rounds differently.
using Plain = typename Dst::PlainObject;
dst.setZero();
dst.noalias() += scale * a * b;
const Plain expected = dst;
dst.setConstant(nan_value<typename Dst::Scalar>());
dst.noalias() = scale * a * b;
VERIFY(dst.allFinite());
if (gemm)
VERIFY(bitwise_equal(dst, expected));
else
VERIFY(verifyProduct(dst, expected, scale * a, b));
// Assignment without noalias goes through a temporary, whose storage order is the product's.
dst.setConstant(nan_value<typename Dst::Scalar>());
dst = scale * a * b;
VERIFY(dst.allFinite());
VERIFY(verifyProduct(dst, expected, scale * a, b));
if (a.cols() > 0) VERIFY(verifyProduct(expected, Plain((scale * a).lazyProduct(b)), scale * a, b));
}
// Sizes around the packet widths and the register and cache blocks of the kernels, at depths below, at and above
// the small blocking's depth block.
template <typename Scalar, int DstOrder>
void assign_shapes() {
using Mat = Matrix<Scalar, Dynamic, Dynamic, ColMajor>;
using DstMat = Matrix<Scalar, Dynamic, Dynamic, DstOrder>;
const Index sizes[] = {1, 3, 16, 17, 32, 33, 48, 65, 129};
const Index depths[] = {1, 25, 64, 130, 300};
for (Index m : sizes)
for (Index n : sizes)
for (Index k : depths) {
if (m * n * k > 1000000) continue;
const Mat a = Mat::Random(m, k), b = Mat::Random(k, n);
DstMat c(m, n);
check_assign(c, a, b, Scalar(1));
if (m > 1 && n > 1) check_assign(c, a, b, Scalar(2));
}
// Transposed operands, and a column-major result read back through an outer stride with sentinel padding.
const Index m = 97, n = 70, k = 300;
const Mat at = Mat::Random(k, m), bt = Mat::Random(n, k);
DstMat c(m, n);
check_assign(c, at.transpose(), bt.transpose(), Scalar(1));
const Mat a = Mat::Random(m, k), b = Mat::Random(k, n);
Mat storage = Mat::Constant(m + 5, n, Scalar(7));
Map<Mat, 0, OuterStride<>> mapped(storage.data(), m, n, OuterStride<>(m + 5));
check_assign(mapped, a, b, Scalar(1));
VERIFY((storage.bottomRows(5).array() == Scalar(7)).all());
}
// Outer stride 1 and inner stride n: a row-major layout seen through column-major strides, so the kernel's C has a
// unit column stride.
template <typename Scalar>
void assign_unit_col_stride() {
using Mat = Matrix<Scalar, Dynamic, Dynamic, ColMajor>;
for (Index m : {Index(17), Index(65), Index(129)})
for (Index n : {Index(9), Index(40), Index(129)})
for (Index k : {Index(25), Index(300)}) {
const Mat a = Mat::Random(m, k), b = Mat::Random(k, n);
Matrix<Scalar, Dynamic, 1> storage = Matrix<Scalar, Dynamic, 1>::Constant(m * n + 3, Scalar(7));
Map<Mat, 0, Stride<Dynamic, Dynamic>> mapped(storage.data(), m, n, Stride<Dynamic, Dynamic>(1, n));
check_assign(mapped, a, b, Scalar(1));
for (Index i = m * n; i < storage.size(); ++i) VERIFY_IS_EQUAL(storage(i), Scalar(7));
}
}
// A depth past the blocking's depth block: only the first pass overwrites, the later ones accumulate.
template <typename Scalar>
void assign_depth_passes() {
using Mat = Matrix<Scalar, Dynamic, Dynamic, ColMajor>;
using Blocking = internal::gemm_blocking_space<ColMajor, Scalar, Scalar, Dynamic, Dynamic, Dynamic>;
for (Index m : {Index(33), Index(129)}) {
const Index n = 40;
Index k = 2 * Blocking(m, n, 1 << 20, 1, true).kc() + 7;
VERIFY(Blocking(m, n, k, 1, true).kc() < k);
const Mat a = Mat::Random(m, k), b = Mat::Random(k, n);
Mat c(m, n);
check_assign(c, a, b, Scalar(1));
}
}
// An inner stride of two: the result coefficients are every other scalar, and the ones in between stay untouched.
template <typename Scalar>
void assign_inner_stride() {
using Mat = Matrix<Scalar, Dynamic, Dynamic, ColMajor>;
const Index m = 65, n = 40, k = 150;
const Mat a = Mat::Random(m, k), b = Mat::Random(k, n);
Matrix<Scalar, Dynamic, 1> storage = Matrix<Scalar, Dynamic, 1>::Constant(2 * m * n, Scalar(7));
Map<Mat, 0, Stride<Dynamic, 2>> mapped(storage.data(), m, n, Stride<Dynamic, 2>(2 * m, 2));
check_assign(mapped, a, b, Scalar(1));
for (Index i = 1; i < storage.size(); i += 2) VERIFY_IS_EQUAL(storage(i), Scalar(7));
}
// Conjugated operands and scalar factors on either side, and a real operand on either side of a complex product.
template <typename Scalar>
void assign_conjugate() {
using Mat = Matrix<Scalar, Dynamic, Dynamic, ColMajor>;
using RealScalar = typename NumTraits<Scalar>::Real;
using RealMat = Matrix<RealScalar, Dynamic, Dynamic, ColMajor>;
const Index m = 70, n = 45, k = 140;
const Mat a = Mat::Random(m, k), b = Mat::Random(k, n), at = Mat::Random(k, m);
Mat c(m, n);
const Scalar s = internal::random<Scalar>();
check_assign(c, a.conjugate(), b, s);
check_assign(c, at.adjoint(), b, s);
check_assign(c, a, (s * b).conjugate(), Scalar(1));
const RealMat ra = RealMat::Random(m, k), rb = RealMat::Random(k, n);
check_assign(c, ra, b, RealScalar(1));
check_assign(c, a, rb, RealScalar(1));
}
// Accumulation is unchanged: += and -= onto a nonzero result.
template <typename Scalar>
void accumulate_unchanged() {
using Mat = Matrix<Scalar, Dynamic, Dynamic, ColMajor>;
const Index m = 66, n = 33, k = 200;
const Mat a = Mat::Random(m, k), b = Mat::Random(k, n), c0 = Mat::Random(m, n);
Mat c = c0;
c.noalias() += a * b;
VERIFY(verifyProduct(Mat(c - c0), Mat(a.lazyProduct(b)), a, b));
c = c0;
c.noalias() -= a * b;
VERIFY(verifyProduct(Mat(c0 - c), Mat(a.lazyProduct(b)), a, b));
}
// alpha * acc is -0 for alpha = -1 and an all-zero product, and overwriting must keep the sign that accumulating into
// a zeroed result gives: +0 for real scalars (0 + (-0)); for complex ones it depends on the packet pmadd (the AVX and
// SSE fmaddsub forms give a -0 real part). Odd sizes reach the scalar row and column tails as well as the packet
// stores.
template <typename Scalar>
void assign_signed_zero() {
using Mat = Matrix<Scalar, Dynamic, Dynamic, ColMajor>;
for (Index size : {64, 67}) {
const Mat a = Mat::Ones(size, size), b = Mat::Zero(size, size);
Mat expected = Mat::Zero(size, size);
expected.noalias() += Scalar(-1) * a * b;
Mat c = Mat::Constant(size, size, nan_value<Scalar>());
c.noalias() = Scalar(-1) * a * b;
VERIFY(bitwise_equal(c, expected));
for (Index j = 0; j < size; ++j)
for (Index i = 0; i < size; ++i)
VERIFY(c(i, j) == Scalar(0) && (NumTraits<Scalar>::IsComplex || !(std::signbit)(numext::real(c(i, j)))));
}
}
// The driver's own contract: overwriting assigns, so a zero factor or an empty depth zeroes the result. The result is
// a padded column-major block whatever the default storage order, and the padding must stay untouched. Through
// gemm_run_overwrite, which takes run_overwrite where the driver has one (not the external-BLAS one).
template <typename Scalar>
void driver_overwrite() {
using Mat = Matrix<Scalar, Dynamic, Dynamic, ColMajor>;
using Gemm =
internal::general_matrix_matrix_product<Index, Scalar, ColMajor, false, Scalar, ColMajor, false, ColMajor, 1>;
using Blocking = internal::gemm_blocking_space<ColMajor, Scalar, Scalar, Dynamic, Dynamic, Dynamic>;
const Index m = 40, n = 30, k = 20, ldc = m + 5;
const Mat a = Mat::Random(m, k), b = Mat::Random(k, n);
Mat storage(ldc, n);
internal::GemmParallelInfo<Index>* no_info = nullptr;
for (Index depth : {Index(0), k}) {
for (Scalar alpha : {Scalar(0), Scalar(1)}) {
Blocking blocking(m, n, depth, 1, true);
storage.setConstant(Scalar(7));
storage.topRows(m).setConstant(nan_value<Scalar>());
internal::gemm_run_overwrite<Gemm>(m, n, depth, a.data(), m, b.data(), k, storage.data(), Index(1), ldc, alpha,
blocking, no_info);
const Mat expected = (depth == 0 || alpha == Scalar(0)) ? Mat(Mat::Zero(m, n)) : Mat(a.lazyProduct(b));
VERIFY(storage.topRows(m).allFinite());
VERIFY(verifyProduct(Mat(storage.topRows(m)), expected, a, b));
VERIFY((storage.bottomRows(ldc - m).array() == Scalar(7)).all());
}
}
// An empty result is left alone.
Blocking blocking(m, 0, k, 1, true);
internal::gemm_run_overwrite<Gemm>(m, Index(0), k, a.data(), m, b.data(), k, storage.data(), Index(1), ldc, Scalar(1),
blocking, no_info);
}
// A kernel from before the overwrite contract: only the untagged call, which accumulates.
struct accumulate_only_kernel {
int* calls;
template <typename... Args>
void operator()(const Args&...) const {
++*calls;
}
};
// A general_matrix_matrix_product specialization from before the contract: no run_overwrite and no CanOverwrite.
struct accumulate_only_gemm {
static int calls;
template <typename Scalar, typename Blocking>
static void run(Index rows, Index cols, Index, const Scalar*, Index, const Scalar*, Index, Scalar* res, Index,
Index resStride, Scalar, Blocking&, internal::GemmParallelInfo<Index>*) {
++calls;
for (Index j = 0; j < cols; ++j)
for (Index i = 0; i < rows; ++i) res[i + j * resStride] += Scalar(1);
}
};
int accumulate_only_gemm::calls = 0;
// A scalar from before the overwrite contract whose product is an expression: it supports `dst += a * c` but not
// adding that expression to a scalar, so accumulating stores must ask for nothing more.
struct et_scalar;
struct et_product {
double v;
};
struct et_scalar {
double v;
et_scalar& operator+=(const et_product& p) {
v += p.v;
return *this;
}
};
et_product operator*(const et_scalar& a, const et_scalar& b) { return et_product{a.v * b.v}; }
// Kernels with and without the profitability hook.
struct pays_never {
static bool overwrite_pays(Index, Index, Index) { return false; }
};
struct no_pays_hook {};
#ifdef EIGEN_VECTORIZE_SME
// A kernel from before the contract that reads its LHS in place: only the untagged run_direct_lhs.
struct accumulate_only_direct_kernel {
int* calls;
template <typename... Args>
void run_direct_lhs(const Args&...) const {
++*calls;
}
};
#endif
// Old interfaces keep working: the kernel only ever gets the untagged call, and a driver without run_overwrite is
// overwritten by zeroing the result and accumulating into it.
void old_interfaces() {
et_scalar dst{1}, alpha{2}, acc{3};
internal::gebp_store_scalar<false>(true, dst, alpha, acc);
VERIFY_IS_EQUAL(dst.v, 7.0);
VERIFY((internal::gebp_overwrite_pays<no_pays_hook>(Index(64), Index(64), Index(64))));
VERIFY(!(internal::gebp_overwrite_pays<pays_never>(Index(64), Index(64), Index(64))));
#ifdef EIGEN_VECTORIZE_SME
int direct_calls = 0;
accumulate_only_direct_kernel direct{&direct_calls};
internal::sme_direct_lhs_call(std::false_type(), direct, true, 1, 2, 3);
VERIFY_IS_EQUAL(direct_calls, 1);
#endif
int calls = 0;
accumulate_only_kernel kernel{&calls};
internal::gebp_run(std::false_type(), kernel, true, 1, 2, 3);
VERIFY_IS_EQUAL(calls, 1);
STATIC_CHECK(!(internal::gebp_kernel_overwrites<accumulate_only_kernel>::value));
STATIC_CHECK(!(internal::gemm_can_overwrite<accumulate_only_gemm>::value));
const Index m = 6, n = 4, k = 3, ldc = 8;
Matrix<float, Dynamic, 1> res = Matrix<float, Dynamic, 1>::Constant(ldc * n, nan_value<float>());
internal::gemm_blocking_space<ColMajor, float, float, Dynamic, Dynamic, Dynamic> blocking(m, n, k, 1, true);
const float* none = nullptr;
internal::GemmParallelInfo<Index>* no_info = nullptr;
accumulate_only_gemm::calls = 0;
internal::gemm_run_overwrite<accumulate_only_gemm>(m, n, k, none, m, none, k, res.data(), Index(1), ldc, 1.f,
blocking, no_info);
VERIFY_IS_EQUAL(accumulate_only_gemm::calls, 1);
for (Index j = 0; j < n; ++j)
for (Index i = 0; i < ldc; ++i) VERIFY(i < m ? res(i + j * ldc) == 1.f : (numext::isnan)(res(i + j * ldc)));
}
// A complex-like custom scalar over a built-in real type.
struct custom_complex {
double re, im;
};
// int and half have no architecture kernel, so they take the generic one: built-in arithmetic opts in, half and
// custom scalars do not (assigning them is not known to match accumulating onto zero). Architecture kernels opt in
// where they implement it.
template <typename Lhs, typename Rhs = Lhs>
using kernel_for =
internal::gebp_kernel<Lhs, Rhs, Index, internal::blas_data_mapper<Lhs, Index, ColMajor>,
internal::gebp_traits<Lhs, Rhs>::mr, internal::gebp_traits<Lhs, Rhs>::nr, false, false>;
void capability_trait() {
STATIC_CHECK((internal::gebp_kernel_overwrites<kernel_for<int>>::value));
STATIC_CHECK(!(internal::gebp_kernel_overwrites<kernel_for<half>>::value));
STATIC_CHECK((internal::gebp_overwritable_scalar<std::complex<double>>::value));
STATIC_CHECK(!(internal::gebp_overwritable_scalar<custom_complex>::value));
#ifdef EIGEN_USE_BLAS
// With beta = 0, ?gemm may form alpha * t and return -0 where zero-then-accumulate gives +0 (reference BLAS does
// in its transposed branches), so the external-BLAS driver keeps zeroing first.
STATIC_CHECK(
!(internal::gemm_can_overwrite<internal::general_matrix_matrix_product<Index, float, ColMajor, false, float,
ColMajor, false, ColMajor, 1>>::value));
STATIC_CHECK(
!(internal::gemm_can_overwrite<internal::general_matrix_matrix_product<Index, double, RowMajor, false, double,
ColMajor, false, ColMajor, 1>>::value));
#endif
#ifdef EIGEN_VECTORIZE_SME
STATIC_CHECK((internal::gebp_kernel_overwrites<kernel_for<float>>::value));
STATIC_CHECK((internal::gebp_kernel_overwrites<kernel_for<std::complex<float>>>::value));
// Complex results overwrite up to 128 KB.
VERIFY((kernel_for<std::complex<float>>::overwrite_pays(128, 128, 64)));
VERIFY(!(kernel_for<std::complex<float>>::overwrite_pays(128, 129, 64)));
#ifdef EIGEN_VECTORIZE_SME_F64F64
// 64-bit results overwrite up to 128 KB; a 32-bit Index must not overflow forming the byte count.
using DoubleKernel = internal::gebp_kernel<double, double, int, internal::blas_data_mapper<double, int, ColMajor>,
internal::gebp_traits<double, double>::mr,
internal::gebp_traits<double, double>::nr, false, false>;
VERIFY(DoubleKernel::overwrite_pays(128, 128, 64));
VERIFY(!DoubleKernel::overwrite_pays(128, 129, 64));
VERIFY(!DoubleKernel::overwrite_pays(16384, 16384, 64));
VERIFY((kernel_for<std::complex<double>>::overwrite_pays(128, 64, 64)));
VERIFY(!(kernel_for<std::complex<double>>::overwrite_pays(128, 65, 64)));
#endif
#endif
}
EIGEN_DECLARE_TEST(product_assign) {
CALL_SUBTEST_1(capability_trait());
CALL_SUBTEST_1(old_interfaces());
CALL_SUBTEST_1((assign_shapes<float, ColMajor>()));
CALL_SUBTEST_1((assign_shapes<float, RowMajor>()));
CALL_SUBTEST_1(assign_inner_stride<float>());
CALL_SUBTEST_1(assign_unit_col_stride<float>());
CALL_SUBTEST_1(assign_depth_passes<float>());
CALL_SUBTEST_1(assign_signed_zero<float>());
CALL_SUBTEST_1(accumulate_unchanged<float>());
CALL_SUBTEST_1(driver_overwrite<float>());
CALL_SUBTEST_2((assign_shapes<double, ColMajor>()));
CALL_SUBTEST_2((assign_shapes<double, RowMajor>()));
CALL_SUBTEST_2(assign_inner_stride<double>());
CALL_SUBTEST_2(assign_unit_col_stride<double>());
CALL_SUBTEST_2(assign_depth_passes<double>());
CALL_SUBTEST_2(assign_signed_zero<double>());
CALL_SUBTEST_2(driver_overwrite<double>());
CALL_SUBTEST_3((assign_shapes<std::complex<float>, ColMajor>()));
CALL_SUBTEST_3((assign_shapes<std::complex<float>, RowMajor>()));
CALL_SUBTEST_3(assign_conjugate<std::complex<float>>());
CALL_SUBTEST_3(assign_inner_stride<std::complex<float>>());
CALL_SUBTEST_3(assign_unit_col_stride<std::complex<float>>());
CALL_SUBTEST_3(assign_depth_passes<std::complex<float>>());
CALL_SUBTEST_3(assign_signed_zero<std::complex<float>>());
CALL_SUBTEST_3(accumulate_unchanged<std::complex<float>>());
CALL_SUBTEST_3(driver_overwrite<std::complex<float>>());
CALL_SUBTEST_4((assign_shapes<std::complex<double>, ColMajor>()));
CALL_SUBTEST_4((assign_shapes<std::complex<double>, RowMajor>()));
CALL_SUBTEST_4(assign_conjugate<std::complex<double>>());
CALL_SUBTEST_4(assign_inner_stride<std::complex<double>>());
CALL_SUBTEST_4(assign_unit_col_stride<std::complex<double>>());
CALL_SUBTEST_4(assign_depth_passes<std::complex<double>>());
CALL_SUBTEST_4(assign_signed_zero<std::complex<double>>());
CALL_SUBTEST_4(driver_overwrite<std::complex<double>>());
}