blob: d48423c93c95e339ba64bb619dc782840f1562d1 [file] [edit]
// 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
// SME GEMM kernel tests.
// Requires a compiler targeting SME2, e.g. -march=armv9.2-a+sme2, which selects
// the backend on its own; -DEIGEN_ARM64_USE_SME then makes a toolchain that
// cannot provide it fail the build rather than fall back to NEON.
// double and complex<double> additionally need FEAT_SME_F64F64 (+sme-f64f64, or
// a -mcpu that implies it); without it EIGEN_VECTORIZE_SME_F64F64 is undefined
// and they keep the generic kernel, so their subtests pack their cases through
// that path instead.
#include "product.h"
#include <cfenv>
// Without the right -march flags, __ARM_FEATURE_SME is undefined and
// EIGEN_VECTORIZE_SME never fires - the test would silently compile
// against the NEON GEBP kernel and pass, making this a useless no-op.
// Fail the build instead.
#if !defined(EIGEN_VECTORIZE_SME)
#error \
"product_sme requires the SME backend. Build with -march=armv9.2-a+sme2 " \
"(see -DEIGEN_TEST_SME=ON in test/CMakeLists.txt for the typical CMake " \
"invocation)."
#endif
// The backend is chosen from the compiler's target features; EIGEN_ARM64_USE_SME,
// which this test's CMake option passes, only turns a toolchain that cannot
// provide it into a build error. Assert that here, so a regression making
// selection depend on the opt-in again is caught rather than masked by it.
#if !defined(EIGEN_ARM64_SME_SELECTED)
#error "the SME backend was forced by EIGEN_ARM64_USE_SME rather than selected from the compiler's target features."
#endif
template <typename Scalar>
using SmeColMajorMat = Matrix<Scalar, Dynamic, Dynamic, ColMajor>;
template <typename Scalar>
using SmeRowMajorMat = Matrix<Scalar, Dynamic, Dynamic, RowMajor>;
template <typename Scalar>
using SmeVector = Matrix<Scalar, Dynamic, 1>;
template <typename Scalar>
using SmeColMajorStridedMat = Map<SmeColMajorMat<Scalar>, 0, Stride<Dynamic, Dynamic>>;
template <typename Scalar>
using SmeRowMajorStridedMat = Map<SmeRowMajorMat<Scalar>, 0, Stride<Dynamic, Dynamic>>;
// The logical micro-kernel block for Scalar (kSmeMr/kSmeNr for float and so
// on). Sizes below are expressed in terms of it so each scalar sweeps its own
// block boundaries; sme_tile() is the ZA tile side at the SVL=512 design point,
// which is where the intra-block splits fall. Complex scalars pair two tiles
// per accumulator, so their block is not square.
template <typename Scalar>
static constexpr int sme_mr() {
return internal::sme_block<Scalar>::mr;
}
template <typename Scalar>
static constexpr int sme_nr() {
return internal::sme_block<Scalar>::nr;
}
template <typename Scalar>
static constexpr int sme_tile() {
return internal::sme_block<Scalar>::mr / internal::sme_block<Scalar>::kGridRows;
}
// svwhilelt_b* is overloaded only on the four fixed-width types, so forwarding
// the caller's own type would leave Index ambiguous wherever it is `long` while
// int64_t is `long long`, and deducing one type from both arguments would reject
// the literal-plus-Index bounds the packers pass. Name every spelling Index
// takes, mix the two argument types, and check the predicate so the traits'
// widening cannot change which lanes are active.
// EIGEN_DONT_INLINE here and below: GCC (14.1 through trunk) inlines a __arm_locally_streaming function into a
// non-streaming caller without its mode switch, running the body as non-streaming SVE at that vector length.
template <typename Scalar, typename Begin, typename End = Begin>
EIGEN_DONT_INLINE __arm_locally_streaming static bool sme_whilelt_covers_first_lane_only() {
using Traits = internal::sme_packet_traits<Scalar>;
const svbool_t pg = Traits::whilelt(Begin(0), End(1));
return svptest_first(Traits::ptrue(), pg) && svcntp_b8(Traits::ptrue(), pg) == 1;
}
template <typename Scalar>
static void test_whilelt_operand_types() {
VERIFY((sme_whilelt_covers_first_lane_only<Scalar, int>()));
VERIFY((sme_whilelt_covers_first_lane_only<Scalar, long>()));
VERIFY((sme_whilelt_covers_first_lane_only<Scalar, long long>()));
VERIFY((sme_whilelt_covers_first_lane_only<Scalar, Index>()));
VERIFY((sme_whilelt_covers_first_lane_only<Scalar, int, Index>()));
}
// Column j of out (n rows, one streaming vector) receives the j-th operation's result.
template <typename Scalar>
EIGEN_DONT_INLINE __arm_locally_streaming static Scalar sme_packet_ops(Scalar* out, const Scalar* a, const Scalar* b,
const Scalar* c, Index active) {
using Traits = internal::sme_packet_traits<Scalar>;
using Vec = typename Traits::type;
const svbool_t all = Traits::ptrue();
const svbool_t pg = Traits::whilelt(0, active);
const Index n = Traits::size();
const Vec va = internal::ploadu(all, a), vb = internal::ploadu(all, b), vc = internal::ploadu(all, c);
internal::pstoreu(all, out, internal::pmadd(all, va, vb, vc));
internal::pstoreu(all, out + n, internal::pnmadd(all, va, vb, vc));
internal::pstoreu(all, out + 2 * n, internal::pmadd_m(pg, va, vb, vc));
internal::pstoreu(all, out + 3 * n, internal::padd(all, va, vb));
internal::pstoreu(all, out + 4 * n, internal::pmul(all, va, vb));
internal::pstoreu(all, out + 5 * n, internal::pnegate(all, va));
internal::pstoreu(pg, out + 6 * n, internal::pset1<Vec>(Scalar(7)));
return internal::predux(pg, va);
}
// Operand order and predication of the packet layer, on small integers so every result is exact.
// a_i = i + 2, b_i = -(i + 3), c_i = 2i + 5 are pairwise distinct and never +-1, so swapping two
// operands of pmadd or pnmadd changes every lane: ab + c - (ac + b) = (b - c)(a - 1), and so on.
template <typename Scalar>
static void test_packet_ops() {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
const Index n = internal::sme_packet_traits<Scalar>::size();
const Vec a = Vec::LinSpaced(n, Scalar(2), Scalar(n + 1));
const Vec b = Vec::LinSpaced(n, Scalar(-3), Scalar(-(n + 2)));
const Vec c = Vec::LinSpaced(n, Scalar(5), Scalar(2 * n + 3));
for (Index active = 0; active <= n; ++active) {
Mat out = Mat::Constant(n, 7, Scalar(-1));
const Scalar sum = sme_packet_ops(out.data(), a.data(), b.data(), c.data(), active);
Mat ref(n, 7);
ref.col(0) = a.cwiseProduct(b) + c;
ref.col(1) = c - a.cwiseProduct(b);
ref.col(2) = c;
ref.col(2).head(active) = ref.col(0).head(active);
ref.col(3) = a + b;
ref.col(4) = a.cwiseProduct(b);
ref.col(5) = -a;
ref.col(6).setConstant(Scalar(-1));
ref.col(6).head(active).setConstant(Scalar(7));
VERIFY_IS_EQUAL(out, ref);
VERIFY_IS_EQUAL(sum, a.head(active).sum());
}
}
// Write one element into a packed panel of width w, in the layout the SME
// kernel reads: a real scalar lands at dst[k*w + r], a complex one splits into
// the depth step's real and imaginary halves.
template <typename Scalar>
static void set_packed(Scalar* panel, Index w, Index k, Index r, const Scalar& v) {
panel[k * w + r] = v;
}
template <typename RealScalar>
static void set_packed(std::complex<RealScalar>* panel, Index w, Index k, Index r, const std::complex<RealScalar>& v) {
RealScalar* p = reinterpret_cast<RealScalar*>(panel + k * w);
p[r] = numext::real(v);
p[w + r] = numext::imag(v);
}
template <typename InputMat, typename ResultMat, typename ResultMap>
static void verify_strided_result(int n, ResultMat& storage, const Stride<Dynamic, Dynamic>& stride) {
InputMat A = InputMat::Random(n, n);
InputMat B = InputMat::Random(n, n);
ResultMap C(storage.data(), n, n, stride);
C = ResultMat::Random(n, n);
ResultMat c_before = C.eval();
C.noalias() += A * B;
ResultMat ref = c_before + (A.lazyProduct(B)).eval();
ResultMat got = C;
VERIFY_IS_APPROX(got, ref);
}
template <typename Scalar, typename InputMat>
static void test_general_strided_result(int n) {
// General-stride C path: InputMat selects the source packers, while both C
// strides are non-unit so sme_store_za_tile uses scalar scatter.
SmeColMajorMat<Scalar> storage = SmeColMajorMat<Scalar>::Zero(2 * n, n);
verify_strided_result<InputMat, SmeColMajorMat<Scalar>, SmeColMajorStridedMat<Scalar>>(
n, storage, Stride<Dynamic, Dynamic>(/*outer=*/2 * n, /*inner=*/2));
// Padding rows skipped by the strided Map should not be touched.
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
VERIFY(storage(2 * i + 1, j) == Scalar(0));
}
}
}
template <typename Scalar>
static void test_rowmajor_strided_result(int n) {
// RowMajor C path: inner stride is one, with padded columns after the Map.
SmeRowMajorMat<Scalar> storage = SmeRowMajorMat<Scalar>::Zero(n, 2 * n);
verify_strided_result<SmeRowMajorMat<Scalar>, SmeRowMajorMat<Scalar>, SmeRowMajorStridedMat<Scalar>>(
n, storage, Stride<Dynamic, Dynamic>(/*outer=*/2 * n, /*inner=*/1));
// Padding columns skipped by the strided Map should not be touched.
for (int i = 0; i < n; ++i) {
for (int j = n; j < 2 * n; ++j) {
VERIFY(storage(i, j) == Scalar(0));
}
}
}
// A non-trivial alpha: complex where the scalar is, so the store's rotation of
// the accumulator is exercised rather than a plain rescale.
template <typename Scalar>
struct nontrivial_alpha_impl {
static Scalar run() { return Scalar(1.375); }
};
template <typename RealScalar>
struct nontrivial_alpha_impl<std::complex<RealScalar>> {
static std::complex<RealScalar> run() { return std::complex<RealScalar>(RealScalar(1.375), RealScalar(-0.625)); }
};
// Conjugated operands and a non-real alpha.
//
// blas_traits folds .adjoint()/.conjugate() and a scalar factor into the
// gebp_kernel's ConjugateLhs/ConjugateRhs and alpha rather than materializing a
// temporary, so these expressions are the only thing that reaches the complex
// kernel's FMOPA/FMOPS sign choices and its alpha-scaled store. The references
// materialize the conjugation first and multiply coefficient-wise, so they do
// not share a code path with what they check.
template <typename Scalar, typename LhsMat, typename RhsMat, typename ResMat>
static void verify_conjugated_products(int n) {
const LhsMat A = LhsMat::Random(n, n);
const RhsMat B = RhsMat::Random(n, n);
const SmeColMajorMat<Scalar> Ap = A, Bp = B;
const SmeColMajorMat<Scalar> Aa = Ap.adjoint().eval(), Ac = Ap.conjugate().eval();
const SmeColMajorMat<Scalar> Ba = Bp.adjoint().eval(), Bc = Bp.conjugate().eval();
const Scalar alpha = nontrivial_alpha_impl<Scalar>::run();
ResMat C(n, n);
C.setZero();
C.noalias() += A.adjoint() * B;
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(Aa.lazyProduct(Bp)));
C.setZero();
C.noalias() += A * B.adjoint();
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(Ap.lazyProduct(Ba)));
C.setZero();
C.noalias() += A.adjoint() * B.adjoint();
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(Aa.lazyProduct(Ba)));
C.setZero();
C.noalias() += A.conjugate() * B;
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(Ac.lazyProduct(Bp)));
C.setZero();
C.noalias() += A * B.conjugate();
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(Ap.lazyProduct(Bc)));
C.setZero();
C.noalias() += A.conjugate() * B.conjugate();
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(Ac.lazyProduct(Bc)));
C.setZero();
C.noalias() += alpha * (A.adjoint() * B);
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(alpha * Aa.lazyProduct(Bp)));
// A deep rectangular product reaches GEMM even for half-tile output widths.
const LhsMat deepA = LhsMat::Random(n, 65);
const RhsMat deepB = RhsMat::Random(65, n);
const ResMat initial = ResMat::Random(n, n);
const SmeColMajorMat<Scalar> reference = deepA.lazyProduct(deepB);
C = initial;
C.noalias() += deepA * deepB;
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(initial + reference));
C = initial;
C.noalias() += alpha * (deepA * deepB);
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(initial + alpha * reference));
// A real alpha takes the complex store's fused path: -1 from -=, and a
// non-unit real scale.
const Scalar real_alpha(typename NumTraits<Scalar>::Real(1.375));
C = initial;
C.noalias() -= deepA * deepB;
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(initial - reference));
C = initial;
C.noalias() += real_alpha * (deepA * deepB);
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(initial + real_alpha * reference));
#ifdef EIGEN_VECTORIZE_SME_F64F64
// RowMajor GEMM normally transposes the computation. Call the packed kernel
// too, so RowMajor C actually reaches its horizontal ZA slice store.
if (n <= sme_tile<Scalar>()) {
std::vector<Scalar> packedA(n * 65), packedB(n * 65);
for (Index k = 0; k < 65; ++k) {
for (Index i = 0; i < n; ++i) {
set_packed(packedA.data(), Index(n), k, i, deepA(i, k));
set_packed(packedB.data(), Index(n), k, i, deepB(k, i));
}
}
for (Scalar scale : {Scalar(1), alpha, Scalar(-1), real_alpha}) {
C = initial;
internal::sme_gebp_dispatch<Scalar, false, false>::run(
C.data(), Index(ResMat::IsRowMajor ? n : 1), Index(ResMat::IsRowMajor ? 1 : n), packedA.data(),
packedB.data(), Index(n), Index(65), Index(n), scale, Index(65), Index(65), Index(0), Index(0));
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), SmeColMajorMat<Scalar>(initial + scale * reference));
}
}
#endif
}
// Exercise the kc split path just above the SME blocking heuristic's depth cap
// (sme_max_kc in GeneralBlockPanelKernel.h, scaled by the scalar width).
template <typename Scalar>
static void test_deep_k_split() {
constexpr int rows = 64;
const int depth = int(2 * (Index(EIGEN_SME_MAX_KC) * Index(sizeof(float)) / Index(sizeof(Scalar)))) + 2;
constexpr int cols = 64;
SmeColMajorMat<Scalar> A = SmeColMajorMat<Scalar>::Random(rows, depth);
SmeColMajorMat<Scalar> B = SmeColMajorMat<Scalar>::Random(depth, cols);
SmeColMajorMat<Scalar> C = SmeColMajorMat<Scalar>::Random(rows, cols);
SmeColMajorMat<Scalar> c_before = C;
C.noalias() += A * B;
VERIFY_IS_APPROX(C, c_before + (A.lazyProduct(B)).eval());
}
// LHS read in place: strides on either side of the 4 KB-multiple rule (packed versus in place), an odd stride
// and base offset, a sub-block at a row offset, and a depth tail, each checked against lazyProduct.
template <typename Scalar>
static void test_direct_lhs() {
const Index kPage = 4096 / Index(sizeof(Scalar));
const Index rows = 97, depth = 101, cols = 70;
const Index strides[] = {rows + 32 + 1, kPage, kPage + 16, 2 * kPage + 16};
for (Index lda : strides) {
for (Index offset = 0; offset <= 1; ++offset) {
SmeVector<Scalar> storage = SmeVector<Scalar>::Zero(lda * depth + 1);
Map<SmeColMajorMat<Scalar>, 0, OuterStride<>> A(storage.data() + offset, rows + 32, depth, OuterStride<>(lda));
A.setRandom();
const SmeColMajorMat<Scalar> B = SmeColMajorMat<Scalar>::Random(depth, cols);
const SmeColMajorMat<Scalar> C0 = SmeColMajorMat<Scalar>::Random(rows + 32, cols);
SmeColMajorMat<Scalar> C = C0;
C.noalias() += A * B;
VERIFY_IS_APPROX(C, C0 + A.lazyProduct(B));
SmeColMajorMat<Scalar> D = C0.middleRows(3, rows);
D.noalias() += A.middleRows(3, rows) * B;
VERIFY_IS_APPROX(D, C0.middleRows(3, rows) + A.middleRows(3, rows).lazyProduct(B));
}
}
#ifdef EIGEN_VECTORIZE_SME
const int units = nbSmeUnits();
VERIFY(units >= 0);
setNbSmeUnits(1);
VERIFY_IS_EQUAL(nbSmeUnits(), 1);
setNbSmeUnits(units);
VERIFY_IS_EQUAL(nbSmeUnits(), units);
#endif
// An LHS packed once for all column blocks, where the first block reads it in place and a narrower last one, whose
// panel spans more than EIGEN_SME_DIRECT_LHS_MAX_SPAN_BYTES, packs.
{
const Index big = Index(EIGEN_SME_DIRECT_LHS_MAX_SPAN_BYTES) / (2048 * Index(sizeof(Scalar))) + 2;
const SmeColMajorMat<Scalar> Abig = SmeColMajorMat<Scalar>::Random(big, 2048);
const auto A = Abig.topRows(256);
const SmeColMajorMat<Scalar> B = SmeColMajorMat<Scalar>::Random(2048, 532);
SmeColMajorMat<Scalar> C(256, 532);
C.noalias() = A * B;
VERIFY_IS_APPROX(C, A.lazyProduct(B));
}
}
// Small K against large M and N (NEON depths and just past them), and a RHS of one panel read in place: up to
// half a panel wide always, up to a full panel within the span limit, with 4 KB-multiple strides.
template <typename Scalar>
static void test_small_k_and_single_panel_rhs() {
const Index MR = sme_mr<Scalar>(), NR = sme_nr<Scalar>();
for (Index k : {1, 2, 3, 5, 8, 17, 24, 25, 40}) {
for (Index mn : {Index(130), 4 * MR + 3}) {
const SmeColMajorMat<Scalar> A = SmeColMajorMat<Scalar>::Random(mn, k), B = SmeColMajorMat<Scalar>::Random(k, mn);
SmeColMajorMat<Scalar> C = SmeColMajorMat<Scalar>::Random(mn, mn);
const SmeColMajorMat<Scalar> C0 = C;
C.noalias() += A * B;
VERIFY_IS_APPROX(C, C0 + A.lazyProduct(B));
}
}
const Index page = 4096 / Index(sizeof(Scalar));
for (Index n : {Index(1), NR / 2 - 1, NR / 2, NR / 2 + 1, NR}) {
for (Index lda : {5 * MR + 3, page, 2 * page + 1}) {
for (Index depth : {Index(40), Index(301)}) {
const Index rows = numext::mini(lda, 5 * MR + 3);
SmeVector<Scalar> storage = SmeVector<Scalar>::Zero(lda * depth);
Map<SmeColMajorMat<Scalar>, 0, OuterStride<>> A(storage.data(), rows, depth, OuterStride<>(lda));
A.setRandom();
const SmeColMajorMat<Scalar> B = SmeColMajorMat<Scalar>::Random(depth, n);
SmeColMajorMat<Scalar> C = SmeColMajorMat<Scalar>::Random(rows, n);
const SmeColMajorMat<Scalar> C0 = C;
C.noalias() += A * B;
VERIFY_IS_APPROX(C, C0 + A.lazyProduct(B));
}
}
}
// A single-panel RHS whose depth block spans more than EIGEN_SME_DIRECT_LHS_MAX_SPAN_BYTES packs the LHS.
const Index kc = Index(EIGEN_SME_MAX_KC) * Index(sizeof(float)) / Index(sizeof(Scalar));
const Index wide = page * Index(std::size_t(EIGEN_SME_DIRECT_LHS_MAX_SPAN_BYTES) / (std::size_t(kc) * 4096) + 1);
SmeVector<Scalar> storage = SmeVector<Scalar>::Zero(wide * kc);
Map<SmeColMajorMat<Scalar>, 0, OuterStride<>> A(storage.data(), 2 * MR + 5, kc, OuterStride<>(wide));
A.setRandom();
const SmeColMajorMat<Scalar> B = SmeColMajorMat<Scalar>::Random(kc, NR);
SmeColMajorMat<Scalar> C = SmeColMajorMat<Scalar>::Zero(2 * MR + 5, NR);
C.noalias() += A * B;
VERIFY_IS_APPROX(C, A.lazyProduct(B));
}
// Disjoint parts per SME unit (2 and 3 units) against a serial product. product_threaded covers the thread pool;
// this reaches the OpenMP branch in an EIGEN_TEST_OPENMP build and runs serially otherwise.
template <typename Scalar>
static void test_disjoint_parts() {
using Col = SmeColMajorMat<Scalar>;
using Row = SmeRowMajorMat<Scalar>;
const int saved = nbSmeUnits();
const int shapes[][3] = {{301, 259, 131}, {200, 40, 1024}, {20, 700, 300}};
for (int units : {2, 3}) {
setNbSmeUnits(units);
for (const auto& sh : shapes) {
const Col A = Col::Random(sh[0], sh[2]), B = Col::Random(sh[2], sh[1]);
const Col ref = A.lazyProduct(B);
Col C(sh[0], sh[1]);
C.noalias() = A * B;
VERIFY_IS_APPROX(C, ref);
Row R(sh[0], sh[1]);
R.noalias() = A * B;
VERIFY_IS_APPROX(Col(R), ref);
Col big = Col::Random(sh[0] + 5, sh[1] + 3);
Col expected = big;
expected.block(2, 1, sh[0], sh[1]) += Scalar(1.5) * ref;
big.block(2, 1, sh[0], sh[1]).noalias() += Scalar(1.5) * A * B;
VERIFY_IS_APPROX(big, expected);
}
}
setNbSmeUnits(saved);
}
// Streaming-mode entry and exit set every FP exception flag: products on each SME path, with exact results, must leave
// the caller's flags as they were.
template <typename Scalar, typename Product>
static void verify_fp_flags_kept(Product product) {
#if defined(EIGEN_HAS_OPENMP) || defined(EIGEN_GEMM_THREADPOOL)
// A threaded build's parallelize_gemm sizes the thread count in floating point, which can raise FE_INEXACT.
const int checked = FE_ALL_EXCEPT & ~FE_INEXACT;
#else
const int checked = FE_ALL_EXCEPT;
#endif
std::feclearexcept(FE_ALL_EXCEPT);
product();
VERIFY_IS_EQUAL(std::fetestexcept(checked), 0);
std::feraiseexcept(FE_INVALID);
product();
VERIFY_IS_EQUAL(std::fetestexcept(checked), FE_INVALID);
std::feclearexcept(FE_ALL_EXCEPT);
}
template <typename Scalar>
static void test_fp_flags() {
using Col = SmeColMajorMat<Scalar>;
using Row = SmeRowMajorMat<Scalar>;
const Index MR = sme_mr<Scalar>(), NR = sme_nr<Scalar>();
const Index m = 4 * MR + 3, n = 3 * NR + 5, k = 131;
// Small integers keep every product exact, so no flag is legitimately raised.
const Col A = Col::Random(m, k).unaryExpr([](const Scalar& x) { return Scalar(numext::round(numext::real(x) * 4)); });
const Col B = Col::Random(k, n).unaryExpr([](const Scalar& x) { return Scalar(numext::round(numext::real(x) * 4)); });
const Col S = Col::Identity(m, m) * Scalar(2) + Col::Ones(m, m), O = Col::Ones(m, n);
// I plus a subdiagonal of ones: the solution of L x = 1 is 1, 0, 1, 0, ..., exact.
Col L = Col::Identity(m, m);
L.diagonal(-1).setOnes();
const Col Tall = Col::Ones(1024, k), Thin = Col::Ones(k, NR / 2);
Col C(m, n), D(1024, NR / 2);
Row R(m, n);
verify_fp_flags_kept<Scalar>([&] { C.noalias() = A * B; });
verify_fp_flags_kept<Scalar>([&] { C.noalias() += A.conjugate() * B; });
verify_fp_flags_kept<Scalar>([&] { R.noalias() = A * B; });
verify_fp_flags_kept<Scalar>([&] { R.noalias() = Row(A) * Row(B); });
verify_fp_flags_kept<Scalar>([&] { D.noalias() = Tall * Thin; });
verify_fp_flags_kept<Scalar>([&] { C.noalias() = S.template selfadjointView<Lower>() * O; });
verify_fp_flags_kept<Scalar>([&] { C.noalias() = S.template triangularView<Upper>() * O; });
verify_fp_flags_kept<Scalar>([&] {
Col X = O;
L.template triangularView<Lower>().solveInPlace(X);
});
}
// Tiny results (the NEON kernel and both sides of its routing), every storage order of A, B and C.
template <typename Scalar, int AO, int BO, int CO>
static void test_tiny_results_order() {
using MA = Matrix<Scalar, Dynamic, Dynamic, AO>;
using MB = Matrix<Scalar, Dynamic, Dynamic, BO>;
using MC = Matrix<Scalar, Dynamic, Dynamic, CO>;
const Index ps = Index(16 / sizeof(Scalar));
for (Index m : {Index(1), Index(2), Index(3), ps, ps + 1, 2 * ps, 2 * ps + 1})
for (Index n : {1, 2, 3, 5, 8, 9})
for (Index k : {1, 5, 6, 7, 15, 16, 17, 64, 2049}) {
const MA A = MA::Random(m, k);
const MB B = MB::Random(k, n);
const MC ref = A.lazyProduct(B);
MC C = MC::Random(m, n);
const MC C0 = C;
C.noalias() = A * B;
VERIFY_IS_APPROX(C, ref);
C = C0;
C.noalias() += A * B;
VERIFY_IS_APPROX(C, C0 + ref);
C = C0;
C.noalias() -= Scalar(2) * A * B;
VERIFY_IS_APPROX(C, C0 - Scalar(2) * ref);
const MA At = A.transpose();
C.noalias() = At.transpose() * B;
VERIFY_IS_APPROX(C, ref);
}
}
template <typename Scalar>
static void test_tiny_results() {
test_tiny_results_order<Scalar, ColMajor, ColMajor, ColMajor>();
test_tiny_results_order<Scalar, RowMajor, ColMajor, ColMajor>();
test_tiny_results_order<Scalar, ColMajor, RowMajor, ColMajor>();
test_tiny_results_order<Scalar, RowMajor, RowMajor, ColMajor>();
test_tiny_results_order<Scalar, ColMajor, ColMajor, RowMajor>();
test_tiny_results_order<Scalar, RowMajor, ColMajor, RowMajor>();
test_tiny_results_order<Scalar, ColMajor, RowMajor, RowMajor>();
test_tiny_results_order<Scalar, RowMajor, RowMajor, RowMajor>();
// Sub-blocks of larger operands, a result with an inner stride, and a Hankel view (outer stride 1 < rows): the
// kernel reads only the rows and columns of the result.
for (Index m : {Index(3), Index(16 / sizeof(Scalar)) + 1})
for (Index n : {3, 7})
for (Index k : {16, 19}) {
const SmeColMajorMat<Scalar> Abig = SmeColMajorMat<Scalar>::Random(m + 5, k + 2);
const SmeRowMajorMat<Scalar> Bbig = SmeRowMajorMat<Scalar>::Random(k + 3, n + 4);
const auto A = Abig.block(2, 1, m, k);
const auto B = Bbig.block(1, 2, k, n);
SmeColMajorMat<Scalar> Cbig = SmeColMajorMat<Scalar>::Random(2 * m, n);
const SmeColMajorMat<Scalar> Cbig0 = Cbig;
Map<SmeColMajorMat<Scalar>, 0, Stride<Dynamic, 2>> C(Cbig.data(), m, n, Stride<Dynamic, 2>(2 * m, 2));
C.noalias() += Scalar(3) * A * B;
SmeColMajorMat<Scalar> ref = Cbig0;
Map<SmeColMajorMat<Scalar>, 0, Stride<Dynamic, 2>>(ref.data(), m, n, Stride<Dynamic, 2>(2 * m, 2)) +=
Scalar(3) * A.lazyProduct(B);
VERIFY_IS_APPROX(Cbig, ref);
std::vector<Scalar> h(std::size_t(k - 1 + m)), g(std::size_t(k - 1 + n));
for (auto& x : h) x = internal::random<Scalar>();
for (auto& x : g) x = internal::random<Scalar>();
Map<const SmeColMajorMat<Scalar>, 0, OuterStride<>> H(h.data(), m, k, OuterStride<>(1));
Map<const SmeRowMajorMat<Scalar>, 0, OuterStride<>> G(g.data(), k, n, OuterStride<>(1));
SmeColMajorMat<Scalar> D(m, n);
D.noalias() = H * G;
VERIFY_IS_APPROX(D, H.lazyProduct(G));
}
// Operands packed tight at the end of their buffer, so a read past the last row or column leaves it.
const Index ps = Index(16 / sizeof(Scalar));
for (Index m : {Index(2), Index(3), ps + 1, 2 * ps})
for (Index n : {2, 3, 5, 7})
for (Index k : {16, 17, 18, 19}) {
std::vector<Scalar> a(std::size_t(m * k)), b(std::size_t(k * n));
Map<SmeColMajorMat<Scalar>> A(a.data(), m, k);
Map<SmeRowMajorMat<Scalar>> B(b.data(), k, n);
A.setRandom();
B.setRandom();
SmeColMajorMat<Scalar> C = SmeColMajorMat<Scalar>::Zero(m, n);
C.noalias() += A * B;
VERIFY_IS_APPROX(C, A.lazyProduct(B));
Map<SmeRowMajorMat<Scalar>> Ar(a.data(), m, k);
Map<SmeColMajorMat<Scalar>> Bc(b.data(), k, n);
C.noalias() = Ar * Bc;
VERIFY_IS_APPROX(C, Ar.lazyProduct(Bc));
}
}
// Every tail width of a deep block, which the ZA transposer packs: a ColMajor RHS tail and, through a RowMajor
// product, a transposed LHS tail.
template <typename Scalar>
static void test_deep_tail_panels() {
const Index MR = sme_mr<Scalar>(), NR = sme_nr<Scalar>();
for (Index depth : {4 * NR, 6 * NR + 3}) {
for (Index tail = 1; tail < NR; ++tail) {
for (Index n : {tail, NR + tail}) {
const SmeColMajorMat<Scalar> A = SmeColMajorMat<Scalar>::Random(MR + 3, depth);
const SmeColMajorMat<Scalar> B = SmeColMajorMat<Scalar>::Random(depth, n);
SmeColMajorMat<Scalar> C = SmeColMajorMat<Scalar>::Random(MR + 3, n);
const SmeColMajorMat<Scalar> C0 = C;
C.noalias() += A * B;
VERIFY_IS_APPROX(C, C0 + A.lazyProduct(B));
const SmeRowMajorMat<Scalar> Ar = SmeRowMajorMat<Scalar>::Random(n, depth);
const SmeRowMajorMat<Scalar> Br = SmeRowMajorMat<Scalar>::Random(depth, MR + 3);
SmeRowMajorMat<Scalar> Cr = SmeRowMajorMat<Scalar>::Random(n, MR + 3);
const SmeRowMajorMat<Scalar> Cr0 = Cr;
Cr.noalias() += Ar * Br;
VERIFY_IS_APPROX(Cr, Cr0 + Ar.lazyProduct(Br));
}
}
}
}
// ---------------------------------------------------------------------------
// Raw packed-buffer tests.
//
// The product tests above validate the packers only transitively through a full
// product, where the gebp_kernel can mask a mispack, or where a bug only shows
// at a specific SVL/region boundary. The tests below call the SME packers
// directly and compare the packed buffer exactly against a scalar reference
// (equality is exact). This pins every region -- in particular a dropped
// row-group in the two-pass trailing transpose, and a SYMM packer that copies
// the unused triangle instead of mirroring -- at whatever SVL the run uses.
// ---------------------------------------------------------------------------
// A distinctive marker for buffer cells the packer must leave untouched, and
// for the unused triangle of a lower-triangular operand. Random values live in
// [-1, 1], so it never collides with a real packed value. The complex marker's
// imaginary part differs from its real one, so a packer that fills only the
// real half of a split depth step is caught.
template <typename Scalar>
struct pack_sentinel_impl {
static Scalar run() { return Scalar(98765); }
};
template <typename RealScalar>
struct pack_sentinel_impl<std::complex<RealScalar>> {
static std::complex<RealScalar> run() { return std::complex<RealScalar>(RealScalar(98765), RealScalar(-54321)); }
};
template <typename Scalar>
static Scalar pack_sentinel() {
return pack_sentinel_impl<Scalar>::run();
}
// ---------------------------------------------------------------------------
// NEON small-block path: sme_gebp_neon called directly on hand-packed panels,
// over every row, column and depth tail of the tile ladder, both conjugation
// flags, a non-trivial alpha, panel-mode stride/offset and a general-stride C.
// ---------------------------------------------------------------------------
template <typename Scalar, bool ConjLhs, bool ConjRhs>
static void verify_neon_small_block(Index rows, Index cols, Index depth, bool panel_mode, bool strided_c) {
const Index MR = sme_mr<Scalar>();
const Index NR = sme_nr<Scalar>();
const Index strideA = panel_mode ? depth + 3 : depth;
const Index strideB = panel_mode ? depth + 5 : depth;
const Index offsetA = panel_mode ? 2 : 0;
const Index offsetB = panel_mode ? 1 : 0;
const SmeColMajorMat<Scalar> A = SmeColMajorMat<Scalar>::Random(rows, depth);
const SmeColMajorMat<Scalar> B = SmeColMajorMat<Scalar>::Random(depth, cols);
SmeVector<Scalar> packedA =
SmeVector<Scalar>::Constant((rows + MR) * strideA + offsetA * MR, pack_sentinel<Scalar>());
SmeVector<Scalar> packedB =
SmeVector<Scalar>::Constant((cols + NR) * strideB + offsetB * NR, pack_sentinel<Scalar>());
for (Index i = 0; i < rows; i += MR) {
const Index w = numext::mini(MR, rows - i);
for (Index k = 0; k < depth; ++k)
for (Index r = 0; r < w; ++r) set_packed(packedA.data() + i * strideA + offsetA * w, w, k, r, A(i + r, k));
}
for (Index j = 0; j < cols; j += NR) {
const Index w = numext::mini(NR, cols - j);
for (Index k = 0; k < depth; ++k)
for (Index c = 0; c < w; ++c) set_packed(packedB.data() + j * strideB + offsetB * w, w, k, c, B(k, j + c));
}
const Scalar alpha = nontrivial_alpha_impl<Scalar>::run();
const Index rs = strided_c ? 2 : 1;
const Index cs = strided_c ? 3 * rows : rows;
SmeVector<Scalar> storage = SmeVector<Scalar>::Random(cs * cols);
SmeColMajorStridedMat<Scalar> C(storage.data(), rows, cols, Stride<Dynamic, Dynamic>(cs, rs));
const SmeColMajorMat<Scalar> c_before = C;
const SmeVector<Scalar> storage_before = storage;
internal::sme_gebp_neon<Scalar, ConjLhs, ConjRhs, Index>(storage.data(), rs, cs, packedA.data(), packedB.data(), rows,
depth, cols, alpha, strideA, strideB, offsetA, offsetB);
const SmeColMajorMat<Scalar> Ac = ConjLhs ? A.conjugate().eval() : A;
const SmeColMajorMat<Scalar> Bc = ConjRhs ? B.conjugate().eval() : B;
const SmeColMajorMat<Scalar> ref = c_before + alpha * Ac.lazyProduct(Bc);
VERIFY_IS_APPROX(SmeColMajorMat<Scalar>(C), ref);
// Cells the strided map skips stay untouched.
if (strided_c) {
for (Index j = 0; j < cols; ++j)
for (Index i = 0; i < rows; ++i)
VERIFY_IS_EQUAL(storage(j * cs + i * rs + 1), storage_before(j * cs + i * rs + 1));
}
}
template <typename Scalar>
static void test_neon_small_blocks() {
const Index PS = Index(internal::packet_traits<typename NumTraits<Scalar>::Real>::size);
const Index MR = sme_mr<Scalar>();
const Index NR = sme_nr<Scalar>();
const Index widths[] = {1, 2, 3, PS, PS + 1, 2 * PS + 1, 4 * PS, 4 * PS + 3, MR - 1, MR, MR + 1, 2 * MR + 5};
const Index col_widths[] = {1, 2, 3, 4, 5, 9, NR - 1, NR, NR + 1, 2 * NR + 2};
const Index depths[] = {1, 2, 3, 4, 5, 8, 9, 35};
for (Index rows : widths) {
for (Index cols : col_widths) {
for (Index depth : depths) {
const bool panel_mode = (rows + cols + depth) % 2 == 0;
const bool strided_c = (rows + depth) % 3 == 0;
verify_neon_small_block<Scalar, false, false>(rows, cols, depth, panel_mode, strided_c);
if (NumTraits<Scalar>::IsComplex) {
verify_neon_small_block<Scalar, true, false>(rows, cols, depth, panel_mode, strided_c);
verify_neon_small_block<Scalar, false, true>(rows, cols, depth, panel_mode, strided_c);
verify_neon_small_block<Scalar, true, true>(rows, cols, depth, panel_mode, strided_c);
}
}
}
}
}
// Lower-triangular n x n operand plus the dense selfadjoint reference the packer
// must emit. The unused triangle is filled with the sentinel so a packer that
// copies the dense matrix and never mirrors fails VERIFY_IS_EQUAL.
// product_selfadjoint_matrix stores the valid triangle where row >= col
// (after the Upper/RowMajor xor), so the packer must read stored(row,col)
// below the diagonal and conj(stored(col,row)) above it. Averaging with the
// adjoint cancels the diagonal's imaginary part exactly, which is what a
// selfadjoint view defines it to be.
template <typename Scalar, int StorageOrder>
static void make_lower_stored_selfadjoint(Index n, Matrix<Scalar, Dynamic, Dynamic, StorageOrder>& stored,
Matrix<Scalar, Dynamic, Dynamic, StorageOrder>& full) {
full = Matrix<Scalar, Dynamic, Dynamic, StorageOrder>::Random(n, n);
full = ((full + full.adjoint()) * Scalar(0.5)).eval();
stored = Matrix<Scalar, Dynamic, Dynamic, StorageOrder>::Constant(n, n, pack_sentinel<Scalar>());
for (Index i = 0; i < n; ++i)
for (Index j = 0; j <= i; ++j) stored(i, j) = full(i, j);
}
// LHS SYMM packer: a square selfadjoint diagonal block of size n, packed into
// uniform mr-wide depth-major panels. Reference: full(i+r, k).
template <typename Scalar, int StorageOrder>
static void verify_symm_pack_lhs(Index n) {
const Index MR = sme_mr<Scalar>();
Matrix<Scalar, Dynamic, Dynamic, StorageOrder> stored, full;
make_lower_stored_selfadjoint<Scalar, StorageOrder>(n, stored, full);
SmeVector<Scalar> packed = SmeVector<Scalar>::Constant(n * n, pack_sentinel<Scalar>());
SmeVector<Scalar> ref = SmeVector<Scalar>::Constant(n * n, pack_sentinel<Scalar>());
for (Index i = 0; i < n; i += MR) {
const Index w = numext::mini(MR, n - i);
for (Index k = 0; k < n; ++k)
for (Index r = 0; r < w; ++r) set_packed(ref.data() + i * n, w, k, r, Scalar(full(i + r, k)));
}
internal::symm_pack_lhs<Scalar, Index, sme_mr<Scalar>(), 1, StorageOrder> pack;
pack(packed.data(), stored.data(), stored.outerStride(), /*cols(depth)=*/n, /*rows=*/n);
VERIFY_IS_EQUAL(packed, ref);
}
// RHS SYMM packer: a depth block [k2, k2 + depth) x cols columns of an N x N
// selfadjoint matrix, packed into nr-wide depth-major panels. Reference:
// full(k2 + k, j + c). A k2 > 0 offset makes the transposed region non-empty,
// so partial-width panels reach the two-pass transpose.
template <typename Scalar, int StorageOrder>
static void verify_symm_pack_rhs(Index N, Index depth, Index cols, Index k2) {
eigen_assert(k2 + depth <= N && cols <= N);
const Index NR = sme_nr<Scalar>();
Matrix<Scalar, Dynamic, Dynamic, StorageOrder> stored, full;
make_lower_stored_selfadjoint<Scalar, StorageOrder>(N, stored, full);
SmeVector<Scalar> packed = SmeVector<Scalar>::Constant(cols * depth, pack_sentinel<Scalar>());
SmeVector<Scalar> ref = SmeVector<Scalar>::Constant(cols * depth, pack_sentinel<Scalar>());
for (Index j = 0; j < cols; j += NR) {
const Index w = numext::mini(NR, cols - j);
for (Index k = 0; k < depth; ++k)
for (Index c = 0; c < w; ++c) set_packed(ref.data() + j * depth, w, k, c, Scalar(full(k2 + k, j + c)));
}
internal::symm_pack_rhs<Scalar, Index, sme_nr<Scalar>(), StorageOrder> pack;
pack(packed.data(), stored.data(), stored.outerStride(), /*rows(depth)=*/depth, /*cols=*/cols, k2);
VERIFY_IS_EQUAL(packed, ref);
}
template <typename Scalar>
static void test_symm_pack() {
// The last panel width sweeps a range of partial widths; at each SVL the
// two-pass trailing transpose (the if->loop fix) fires when a partial width
// leaves a trailing row-group remainder in (svl, 2*svl). The spread below
// hits that for svl in {2, 4, 8, 16, 32, 64} -- fp32 SVL 128..2048 and the
// fp64 lane counts, which are half of those.
const int sizes[] = {1, 5, 7, 17, 31, 32, 33, 37, 39, 45, 48, 49, 55, 57, 63, 64, 65, 79, 96, 97};
for (int n : sizes) {
verify_symm_pack_lhs<Scalar, ColMajor>(n);
verify_symm_pack_lhs<Scalar, RowMajor>(n);
// RHS, single depth block anchored at the diagonal (k2 == 0).
verify_symm_pack_rhs<Scalar, ColMajor>(n, n, n, 0);
verify_symm_pack_rhs<Scalar, RowMajor>(n, n, n, 0);
}
// RHS depth blocks offset from the diagonal (k2 > 0): the transposed region is
// non-empty, so the RowMajor operand drives partial-width panels through the
// two-pass transpose and the ColMajor operand through the partial copy.
struct RhsCase {
int N, depth, cols, k2;
};
const RhsCase rhs_cases[] = {
{100, 32, 39, 16}, {100, 24, 39, 32}, {100, 40, 64, 8}, {100, 39, 39, 33}, {128, 57, 57, 40}, {128, 33, 45, 60},
};
for (const RhsCase& c : rhs_cases) {
verify_symm_pack_rhs<Scalar, ColMajor>(c.N, c.depth, c.cols, c.k2);
verify_symm_pack_rhs<Scalar, RowMajor>(c.N, c.depth, c.cols, c.k2);
}
}
// ---------------------------------------------------------------------------
// Mapper-based packing fallback (sme_pack_{lhs,rhs}_fallback).
//
// Taken by by-value tensor sub-mappers and inner-strided blas mappers, which
// the raw pointer + stride packers cannot walk. The product suite reaches these
// only through tensor contractions (nightly, SVL=512) and TriangularSolver
// (random sizes), so here we drive the fallback directly with both mapper
// families and compare the packed buffer against a scalar reference.
// ---------------------------------------------------------------------------
// Minimal stand-ins for by-value sub-mappers. ColMajor packets advance the
// first index, while RowMajor packets follow the normal storage-inner second
// index. operator() returns by value so both take the no-direct-access dispatch.
template <typename Scalar>
struct ByValueColMajorLhsMapper {
const Scalar* data;
Index stride; // element(i, k) = data[i + k * stride], contiguous in i
Scalar operator()(Index i, Index k) const { return data[i + k * stride]; }
template <typename Packet>
EIGEN_ALWAYS_INLINE Packet loadPacket(Index i, Index k) const {
return internal::ploadu<Packet>(data + i + k * stride);
}
};
template <typename Scalar>
struct ByValueRowMajorLhsMapper {
const Scalar* data;
Index stride; // element(i, k) = data[i * stride + k], contiguous in k
Scalar operator()(Index i, Index k) const { return data[i * stride + k]; }
template <typename Packet>
EIGEN_ALWAYS_INLINE Packet loadPacket(Index i, Index k) const {
return internal::ploadu<Packet>(data + i * stride + k);
}
};
template <typename Scalar>
struct ByValueColMajorRhsMapper {
struct LinearMapper {
const Scalar* p; // &element(0, col); contiguous in depth
Scalar operator()(Index k) const { return p[k]; }
template <typename Packet>
EIGEN_ALWAYS_INLINE Packet loadPacket(Index k) const {
return internal::ploadu<Packet>(p + k);
}
};
const Scalar* data;
Index stride; // element(k, col) = data[k + col * stride], contiguous in k
Scalar operator()(Index k, Index col) const { return data[k + col * stride]; }
LinearMapper getLinearMapper(Index k, Index col) const { return LinearMapper{data + k + col * stride}; }
};
template <typename Scalar>
struct ByValueRowMajorRhsMapper {
struct LinearMapper {
const Scalar* p; // &element(row, col); packet offsets advance columns
Scalar operator()(Index offset) const { return p[offset]; }
template <typename Packet>
EIGEN_ALWAYS_INLINE Packet loadPacket(Index offset) const {
return internal::ploadu<Packet>(p + offset);
}
};
const Scalar* data;
Index stride; // element(k, col) = data[k * stride + col], contiguous in col
Scalar operator()(Index k, Index col) const { return data[k * stride + col]; }
LinearMapper getLinearMapper(Index k, Index col) const { return LinearMapper{data + k * stride + col}; }
};
// Length of the packed LHS buffer for the given panel-mode layout, matching the
// dst_panel formula in sme_pack_lhs_fallback.
template <bool PanelMode>
static Index packed_len(Index outer, Index depth, Index unit, Index dst_stride, Index dst_offset) {
// `outer` is rows (LHS) or cols (RHS); `unit` is the panel width mr or nr.
if (!PanelMode) return outer * depth;
Index end = 0;
for (Index i = 0; i < outer; i += unit) {
const Index w = numext::mini(unit, outer - i);
end = numext::maxi(end, i * dst_stride + dst_offset * w + depth * w);
}
return end;
}
template <typename Scalar, bool PanelMode, bool Conjugate, typename MatrixType>
static void fill_lhs_ref(SmeVector<Scalar>& ref, const MatrixType& V, Index rows, Index depth, Index dst_stride,
Index dst_offset) {
const Index MR = sme_mr<Scalar>();
ref.setConstant(pack_sentinel<Scalar>());
for (Index i = 0; i < rows; i += MR) {
const Index w = numext::mini(MR, rows - i);
const Index base = PanelMode ? i * dst_stride + dst_offset * w : i * depth;
for (Index k = 0; k < depth; ++k)
for (Index r = 0; r < w; ++r) {
const Scalar v = V(i + r, k);
set_packed(ref.data() + base, w, k, r, Conjugate ? numext::conj(v) : v);
}
}
}
template <typename Scalar, bool PanelMode, bool Conjugate, typename MatrixType>
static void fill_rhs_ref(SmeVector<Scalar>& ref, const MatrixType& V, Index cols, Index depth, Index dst_stride,
Index dst_offset) {
const Index NR = sme_nr<Scalar>();
ref.setConstant(pack_sentinel<Scalar>());
for (Index j = 0; j < cols; j += NR) {
const Index w = numext::mini(NR, cols - j);
const Index base = PanelMode ? j * dst_stride + dst_offset * w : j * depth;
for (Index k = 0; k < depth; ++k)
for (Index c = 0; c < w; ++c) {
const Scalar v = V(k, j + c);
set_packed(ref.data() + base, w, k, c, Conjugate ? numext::conj(v) : v);
}
}
}
// Every packer check below is the same nine lines -- fill a sentinel buffer,
// build the scalar reference, run the packer, compare exactly -- over a
// different mapper. The mapper is what each check is really about, so it is
// built by the caller and the rest lives here once.
template <typename Scalar, int StorageOrder, bool PanelMode, bool Conjugate, typename Mapper, typename MatrixType>
static void check_packed_lhs(const Mapper& mapper, const MatrixType& V, Index rows, Index depth) {
const Index dst_stride = PanelMode ? depth + 5 : 0;
const Index dst_offset = PanelMode ? 3 : 0;
const Index len = packed_len<PanelMode>(rows, depth, sme_mr<Scalar>(), dst_stride, dst_offset);
SmeVector<Scalar> packed = SmeVector<Scalar>::Constant(len, pack_sentinel<Scalar>());
SmeVector<Scalar> ref(len);
fill_lhs_ref<Scalar, PanelMode, Conjugate>(ref, V, rows, depth, dst_stride, dst_offset);
internal::gemm_pack_lhs<Scalar, Index, Mapper, sme_mr<Scalar>(), 1, typename internal::packet_traits<Scalar>::type,
StorageOrder, Conjugate, PanelMode>
pack;
pack(packed.data(), mapper, depth, rows, dst_stride, dst_offset);
VERIFY_IS_EQUAL(packed, ref);
}
template <typename Scalar, int StorageOrder, bool PanelMode, bool Conjugate, typename Mapper, typename MatrixType>
static void check_packed_rhs(const Mapper& mapper, const MatrixType& V, Index depth, Index cols) {
const Index dst_stride = PanelMode ? depth + 5 : 0;
const Index dst_offset = PanelMode ? 3 : 0;
const Index len = packed_len<PanelMode>(cols, depth, sme_nr<Scalar>(), dst_stride, dst_offset);
SmeVector<Scalar> packed = SmeVector<Scalar>::Constant(len, pack_sentinel<Scalar>());
SmeVector<Scalar> ref(len);
fill_rhs_ref<Scalar, PanelMode, Conjugate>(ref, V, cols, depth, dst_stride, dst_offset);
internal::gemm_pack_rhs<Scalar, Index, Mapper, sme_nr<Scalar>(), StorageOrder, Conjugate, PanelMode> pack;
pack(packed.data(), mapper, depth, cols, dst_stride, dst_offset);
VERIFY_IS_EQUAL(packed, ref);
}
// The raw pointer + stride packers, taken whenever the mapper grants direct
// unit-inner-stride access.
template <typename Scalar, int StorageOrder, bool PanelMode, bool Conjugate>
static void verify_pack_lhs_direct(Index rows, Index depth) {
using MatrixType = Matrix<Scalar, Dynamic, Dynamic, StorageOrder>;
using Mapper = internal::const_blas_data_mapper<Scalar, Index, StorageOrder>;
MatrixType V = MatrixType::Random(rows, depth);
check_packed_lhs<Scalar, StorageOrder, PanelMode, Conjugate>(Mapper(V.data(), V.outerStride()), V, rows, depth);
}
template <typename Scalar, int StorageOrder, bool PanelMode, bool Conjugate>
static void verify_pack_rhs_direct(Index depth, Index cols) {
using MatrixType = Matrix<Scalar, Dynamic, Dynamic, StorageOrder>;
using Mapper = internal::const_blas_data_mapper<Scalar, Index, StorageOrder>;
MatrixType V = MatrixType::Random(depth, cols);
check_packed_rhs<Scalar, StorageOrder, PanelMode, Conjugate>(Mapper(V.data(), V.outerStride()), V, depth, cols);
}
// Inner-strided blas mapper LHS: element(i, k) laid out with inner stride
// `incr`. ColMajor takes the vectorised gather path; RowMajor takes the scalar
// path (its packets would run along depth, not rows).
template <typename Scalar, int StorageOrder, bool PanelMode, bool Conjugate>
static void verify_fallback_lhs_strided(Index rows, Index depth, Index incr) {
using Mapper = internal::blas_data_mapper<Scalar, Index, StorageOrder, Unaligned, Dynamic>;
Matrix<Scalar, Dynamic, Dynamic> V = Matrix<Scalar, Dynamic, Dynamic>::Random(rows, depth);
const Index mstride = (StorageOrder == ColMajor ? rows : depth) * incr;
SmeVector<Scalar> buf = SmeVector<Scalar>::Zero((StorageOrder == ColMajor ? depth : rows) * mstride + incr);
for (Index k = 0; k < depth; ++k)
for (Index i = 0; i < rows; ++i)
buf[StorageOrder == ColMajor ? i * incr + k * mstride : k * incr + i * mstride] = V(i, k);
check_packed_lhs<Scalar, StorageOrder, PanelMode, Conjugate>(Mapper(buf.data(), mstride, incr), V, rows, depth);
}
// By-value LHS mappers exercise both packet directions. RowMajor must stay
// scalar because its packets advance depth rather than rows.
template <typename Scalar, int StorageOrder, bool PanelMode, bool Conjugate>
static void verify_fallback_lhs_byvalue(Index rows, Index depth) {
using MatrixType = Matrix<Scalar, Dynamic, Dynamic, StorageOrder>;
using Mapper = typename std::conditional<StorageOrder == ColMajor, ByValueColMajorLhsMapper<Scalar>,
ByValueRowMajorLhsMapper<Scalar>>::type;
MatrixType V = MatrixType::Random(rows, depth);
check_packed_lhs<Scalar, StorageOrder, PanelMode, Conjugate>(Mapper{V.data(), V.outerStride()}, V, rows, depth);
}
// Inner-strided blas mapper RHS: element(k, col) with inner stride `incr`.
// ColMajor takes the vectorised transpose path; RowMajor takes the scalar path.
template <typename Scalar, int StorageOrder, bool PanelMode, bool Conjugate>
static void verify_fallback_rhs_strided(Index depth, Index cols, Index incr) {
using Mapper = internal::blas_data_mapper<Scalar, Index, StorageOrder, Unaligned, Dynamic>;
Matrix<Scalar, Dynamic, Dynamic> V = Matrix<Scalar, Dynamic, Dynamic>::Random(depth, cols);
const Index mstride = (StorageOrder == ColMajor ? depth : cols) * incr;
SmeVector<Scalar> buf = SmeVector<Scalar>::Zero((StorageOrder == ColMajor ? cols : depth) * mstride + incr);
for (Index col = 0; col < cols; ++col)
for (Index k = 0; k < depth; ++k)
buf[StorageOrder == ColMajor ? k * incr + col * mstride : col * incr + k * mstride] = V(k, col);
check_packed_rhs<Scalar, StorageOrder, PanelMode, Conjugate>(Mapper(buf.data(), mstride, incr), V, depth, cols);
}
// By-value RHS mappers likewise cover both packet directions. RowMajor packets
// advance columns, so the depth-oriented transpose fallback must stay scalar.
template <typename Scalar, int StorageOrder, bool PanelMode, bool Conjugate>
static void verify_fallback_rhs_byvalue(Index depth, Index cols) {
using MatrixType = Matrix<Scalar, Dynamic, Dynamic, StorageOrder>;
using Mapper = typename std::conditional<StorageOrder == ColMajor, ByValueColMajorRhsMapper<Scalar>,
ByValueRowMajorRhsMapper<Scalar>>::type;
MatrixType V = MatrixType::Random(depth, cols);
check_packed_rhs<Scalar, StorageOrder, PanelMode, Conjugate>(Mapper{V.data(), V.outerStride()}, V, depth, cols);
}
// Each packer is swept over all four (PanelMode, Conjugate) combinations.
// Conjugate=true is the identity on real scalars; for complex ones it negates
// the packed imaginary half, and reaches the direct packers from the SYMM
// above-diagonal transposed pack and the fallbacks from an inner-strided
// selfadjoint operand.
template <typename Scalar, int SO>
static void sweep_pack_direct(Index n, Index depth) {
verify_pack_lhs_direct<Scalar, SO, false, false>(n, depth);
verify_pack_lhs_direct<Scalar, SO, false, true>(n, depth);
verify_pack_lhs_direct<Scalar, SO, true, false>(n, depth);
verify_pack_lhs_direct<Scalar, SO, true, true>(n, depth);
verify_pack_rhs_direct<Scalar, SO, false, false>(depth, n);
verify_pack_rhs_direct<Scalar, SO, false, true>(depth, n);
verify_pack_rhs_direct<Scalar, SO, true, false>(depth, n);
verify_pack_rhs_direct<Scalar, SO, true, true>(depth, n);
}
template <typename Scalar, int SO>
static void sweep_fallback_strided(Index n, Index depth, Index incr) {
verify_fallback_lhs_strided<Scalar, SO, false, false>(n, depth, incr);
verify_fallback_lhs_strided<Scalar, SO, false, true>(n, depth, incr);
verify_fallback_lhs_strided<Scalar, SO, true, false>(n, depth, incr);
verify_fallback_lhs_strided<Scalar, SO, true, true>(n, depth, incr);
verify_fallback_rhs_strided<Scalar, SO, false, false>(depth, n, incr);
verify_fallback_rhs_strided<Scalar, SO, false, true>(depth, n, incr);
verify_fallback_rhs_strided<Scalar, SO, true, false>(depth, n, incr);
verify_fallback_rhs_strided<Scalar, SO, true, true>(depth, n, incr);
}
template <typename Scalar, int SO>
static void sweep_fallback_byvalue(Index n, Index depth) {
verify_fallback_lhs_byvalue<Scalar, SO, false, false>(n, depth);
verify_fallback_lhs_byvalue<Scalar, SO, false, true>(n, depth);
verify_fallback_lhs_byvalue<Scalar, SO, true, false>(n, depth);
verify_fallback_lhs_byvalue<Scalar, SO, true, true>(n, depth);
verify_fallback_rhs_byvalue<Scalar, SO, false, false>(depth, n);
verify_fallback_rhs_byvalue<Scalar, SO, false, true>(depth, n);
verify_fallback_rhs_byvalue<Scalar, SO, true, false>(depth, n);
verify_fallback_rhs_byvalue<Scalar, SO, true, true>(depth, n);
}
template <typename Scalar>
static void test_pack_direct() {
const int TILE = sme_tile<Scalar>();
const int MR = sme_mr<Scalar>();
const int NR = sme_nr<Scalar>();
// Widths around the tile side and both panel widths, which differ for
// complex scalars.
const int widths[] = {1, TILE - 1, TILE, TILE + 1, MR, MR + 1, NR, NR + 1, 2 * NR + 1};
// Depths up to sme_neon_max_depth (24 / 16 / 16 / 8 by scalar) reach pack_neon,
// the deeper ones the streaming pack_direct and its predicated tails.
const int depths[] = {1, 3, 8, 9, 17, 25, 27, 35};
for (int d : depths) {
for (int n : widths) {
sweep_pack_direct<Scalar, ColMajor>(n, d);
sweep_pack_direct<Scalar, RowMajor>(n, d);
}
}
// Four-panel LHS copies with leftover panels and tails, over several transposing depth chunks.
for (int d : {35, 67, 97}) {
for (int n : {4 * MR, 5 * MR + 1, 8 * MR + 3}) {
sweep_pack_direct<Scalar, ColMajor>(n, d);
sweep_pack_direct<Scalar, RowMajor>(n, d);
}
}
}
template <typename Scalar>
static void test_mapper_fallback() {
const int MR = sme_mr<Scalar>();
const int widths[] = {4, 5, MR, MR + 1, 2 * MR + 1}; // rows/cols around 4 and the panel width
const int depths[] = {1, 3, 8, 35}; // depth remainders 1..3 and larger
for (int n : widths) {
for (int d : depths) {
// RowMajor mappers take the scalar path: their packets advance the index
// the fallback does not transpose.
for (int incr : {2, 3}) {
sweep_fallback_strided<Scalar, ColMajor>(n, d, incr);
sweep_fallback_strided<Scalar, RowMajor>(n, d, incr);
}
sweep_fallback_byvalue<Scalar, ColMajor>(n, d);
sweep_fallback_byvalue<Scalar, RowMajor>(n, d);
}
}
}
// ---------------------------------------------------------------------------
// Product-level coverage, swept relative to the scalar's own block width.
// ---------------------------------------------------------------------------
// Sizes that land just on and off the block tails and the intra-block ZA-tile
// splits, for both panel widths (they differ for complex scalars) and the tile
// side at the SVL=512 design point.
template <typename Scalar>
static std::vector<int> sme_edge_sizes() {
const int T = sme_tile<Scalar>();
const int MR = sme_mr<Scalar>();
const int NR = sme_nr<Scalar>();
std::vector<int> sizes = {T / 2, T + T / 2, 1, T - 1, T, T + 1, MR - 1, MR, MR + 1,
NR - 1, NR, NR + 1, 2 * MR - 1, 2 * MR, 2 * MR + 1, 2 * NR - 1, 2 * NR, 2 * NR + 1};
std::sort(sizes.begin(), sizes.end());
sizes.erase(std::unique(sizes.begin(), sizes.end()), sizes.end());
return sizes;
}
// The dimension-sum and output-area bounds in GeneralProduct.h route a small
// square product to the coeff-based evaluator, so the n x n sweeps above stop
// reaching the SME kernel once n gets small -- which is exactly where the
// intra-block ZA tile-split edges (T-1, T, T+1) live for double and
// complex<double>. Repeat the same widths over a depth that clears both
// bounds, so every m and n edge still reaches the kernel, and check the
// conjugated forms too: at those sizes test_conjugated_products compares a
// lazyProduct against a lazyProduct and cannot fail.
template <typename Scalar>
static void test_edge_sizes_deep_k() {
const int depth = 4 * sme_mr<Scalar>();
for (int n : sme_edge_sizes<Scalar>()) {
if (n < 2) continue;
const SmeColMajorMat<Scalar> A = SmeColMajorMat<Scalar>::Random(n, depth);
const SmeColMajorMat<Scalar> B = SmeColMajorMat<Scalar>::Random(depth, n);
const SmeColMajorMat<Scalar> Aa = A.adjoint().eval();
const SmeColMajorMat<Scalar> Ba = B.adjoint().eval();
const SmeColMajorMat<Scalar> c_before = SmeColMajorMat<Scalar>::Random(n, n);
const SmeColMajorMat<Scalar> expected = c_before + A.lazyProduct(B);
SmeColMajorMat<Scalar> C = c_before;
C.noalias() += A * B;
VERIFY_IS_APPROX(C, expected);
C = c_before;
C.noalias() += Aa.adjoint() * B;
VERIFY_IS_APPROX(C, expected);
C = c_before;
C.noalias() += A * Ba.adjoint();
VERIFY_IS_APPROX(C, expected);
}
}
template <typename Scalar>
static void test_conjugated_products() {
for (int n : sme_edge_sizes<Scalar>()) {
verify_conjugated_products<Scalar, SmeColMajorMat<Scalar>, SmeColMajorMat<Scalar>, SmeColMajorMat<Scalar>>(n);
verify_conjugated_products<Scalar, SmeRowMajorMat<Scalar>, SmeColMajorMat<Scalar>, SmeColMajorMat<Scalar>>(n);
verify_conjugated_products<Scalar, SmeColMajorMat<Scalar>, SmeRowMajorMat<Scalar>, SmeRowMajorMat<Scalar>>(n);
verify_conjugated_products<Scalar, SmeRowMajorMat<Scalar>, SmeRowMajorMat<Scalar>, SmeColMajorMat<Scalar>>(n);
}
}
template <typename Scalar>
static void test_products() {
const int MR = sme_mr<Scalar>();
// Square edge cases around the block and tile boundaries.
for (int n : sme_edge_sizes<Scalar>()) product(SmeColMajorMat<Scalar>(n, n));
// Thin / wide rectangular cases (M x 1, 1 x N) and non-square cases that
// exercise tail paths for both M and N.
product(SmeColMajorMat<Scalar>(MR, 1));
product(SmeColMajorMat<Scalar>(1, MR));
product(SmeColMajorMat<Scalar>(1, 2 * MR));
product(SmeColMajorMat<Scalar>(2 * MR, 1));
product(SmeColMajorMat<Scalar>(MR + 1, 2 * MR + 1));
product(SmeColMajorMat<Scalar>(2 * MR + 1, MR + 1));
product(SmeColMajorMat<Scalar>(MR - 1, 2 * MR - 1));
product(SmeColMajorMat<Scalar>(MR + 1, 7));
product(SmeColMajorMat<Scalar>(7, MR + 1));
product(SmeColMajorMat<Scalar>(4 * MR, 3));
product(SmeColMajorMat<Scalar>(3, 4 * MR));
test_deep_k_split<Scalar>();
test_edge_sizes_deep_k<Scalar>();
// Random sizes
for (int i = 0; i < g_repeat; i++) {
product(SmeColMajorMat<Scalar>(internal::random<int>(1, EIGEN_TEST_MAX_SIZE),
internal::random<int>(1, EIGEN_TEST_MAX_SIZE)));
}
// Exercise the RowMajor packers and RowMajor result path. When the input
// MatrixType is RowMajor, product() instantiates m1/m2/m3/res in RowMajor,
// so every matrix-matrix product in the suite flows through:
// - the RowMajor LHS packer (gemm_pack_lhs<..., RowMajor>)
// - the RowMajor RHS packer (gemm_pack_rhs<..., RowMajor>)
// - the RowMajor-C dispatch in GeneralMatrixMatrix.h (which transposes
// the computation: C^T = B^T * A^T).
for (int n : sme_edge_sizes<Scalar>()) {
if (n > 1) product(SmeRowMajorMat<Scalar>(n, n));
}
product(SmeRowMajorMat<Scalar>(MR + 1, 2 * MR + 1));
product(SmeRowMajorMat<Scalar>(2 * MR + 1, MR + 1));
for (int i = 0; i < g_repeat; i++) {
product(SmeRowMajorMat<Scalar>(internal::random<int>(1, EIGEN_TEST_MAX_SIZE),
internal::random<int>(1, EIGEN_TEST_MAX_SIZE)));
}
// Exercise the general-stride branch of sme_store_za_tile: fires when both
// C_stride_row != 1 and C_stride_col != 1, e.g. a Map<Matrix> with an
// explicit non-unit inner stride. product.h never builds such a result, so
// without this subtest the scalar-scatter path is effectively untested.
for (int n : sme_edge_sizes<Scalar>()) {
if (n < 2) continue;
test_general_strided_result<Scalar, SmeColMajorMat<Scalar>>(n);
test_general_strided_result<Scalar, SmeRowMajorMat<Scalar>>(n);
test_rowmajor_strided_result<Scalar>(n);
}
// Row-LHS x Row-RHS -> Col-C: the one LHS/RHS/C storage combination that
// product.h's transpose-style expressions never build directly (it always
// flips one side of the multiplication). The code paths are the same as
// other combinations via Eigen's dispatch, but exercise them explicitly.
for (int n : sme_edge_sizes<Scalar>()) {
if (n < 2) continue;
SmeRowMajorMat<Scalar> A = SmeRowMajorMat<Scalar>::Random(n, n);
SmeRowMajorMat<Scalar> B = SmeRowMajorMat<Scalar>::Random(n, n);
SmeColMajorMat<Scalar> C = SmeColMajorMat<Scalar>::Zero(n, n);
C.noalias() += A * B;
VERIFY_IS_APPROX(C, (A.lazyProduct(B)).eval());
}
}
// Folding C into the complex alpha chain, (c_re + re*ar) - im*ai, overflows
// for acc = (h,h), alpha = (1,1), C = (3h,0) although the exact result
// (3h,2h) is finite (#3139 review). Widths straddle the one-vector slice
// width, which selects the store form; a real alpha takes the fused store.
// RowMajor GEMM transposes the computation, so the packed kernel is also
// called directly to reach the horizontal slice store.
template <typename Real, int Order>
static void test_complex_alpha_range() {
using Scalar = std::complex<Real>;
using Mat = Matrix<Scalar, Dynamic, Dynamic, Order>;
const int svl = internal::sme_packet_traits<Real>::size();
const Real h = std::ldexp(Real(1), std::numeric_limits<Real>::max_exponent - 2);
const Index depth = 65;
const Scalar initial(3 * h, 0);
const Scalar alphas[] = {Scalar(1, 1), Scalar(-1, 0)};
const Scalar expected[] = {Scalar(3 * h, 2 * h), Scalar(2 * h, -h)};
for (int n : {svl / 2 - 1, svl / 2, svl / 2 + 1, svl, svl + 1}) {
if (n < 2) continue;
Mat a = Mat::Zero(n, depth), b = Mat::Zero(depth, n);
a.col(0).setConstant(Scalar(h, h));
b.row(0).setOnes();
const Mat ac = a.conjugate(), bc = b.conjugate();
for (int t = 0; t < 2; ++t) {
const Scalar alpha = alphas[t];
for (int conjugation = 0; conjugation < 4; ++conjugation) {
Mat c = Mat::Constant(n, n, initial);
if (conjugation == 0) c.noalias() += alpha * (a * b);
if (conjugation == 1) c.noalias() += alpha * (ac.conjugate() * b);
if (conjugation == 2) c.noalias() += alpha * (a * bc.conjugate());
if (conjugation == 3) c.noalias() += alpha * (ac.conjugate() * bc.conjugate());
for (Index j = 0; j < n; ++j) {
for (Index i = 0; i < n; ++i) {
VERIFY((std::isfinite)(c(i, j).real()) && (std::isfinite)(c(i, j).imag()));
VERIFY(c(i, j) == expected[t]);
}
}
}
// One packed panel; a larger runtime SVL may exceed its static width.
if (n <= numext::mini(sme_mr<Scalar>(), sme_nr<Scalar>())) {
std::vector<Scalar> pa(n * depth), pb(n * depth);
for (Index k = 0; k < depth; ++k) {
for (Index i = 0; i < n; ++i) {
set_packed(pa.data(), Index(n), k, i, a(i, k));
set_packed(pb.data(), Index(n), k, i, b(k, i));
}
}
Mat c = Mat::Constant(n, n, initial);
internal::sme_gebp_dispatch<Scalar, false, false>::run(
c.data(), Index(Order == RowMajor ? n : 1), Index(Order == RowMajor ? 1 : n), pa.data(), pb.data(),
Index(n), depth, Index(n), alpha, depth, depth, Index(0), Index(0));
for (Index j = 0; j < n; ++j) {
for (Index i = 0; i < n; ++i) {
VERIFY((std::isfinite)(c(i, j).real()) && (std::isfinite)(c(i, j).imag()));
VERIFY(c(i, j) == expected[t]);
}
}
}
}
}
}
EIGEN_DECLARE_TEST(product_sme) {
CALL_SUBTEST_1(test_whilelt_operand_types<float>());
CALL_SUBTEST_1(test_packet_ops<float>());
CALL_SUBTEST_1(test_products<float>());
CALL_SUBTEST_1(test_conjugated_products<float>());
CALL_SUBTEST_1(test_symm_pack<float>());
CALL_SUBTEST_1(test_pack_direct<float>());
CALL_SUBTEST_1(test_mapper_fallback<float>());
CALL_SUBTEST_1(test_neon_small_blocks<float>());
CALL_SUBTEST_1(test_direct_lhs<float>());
CALL_SUBTEST_1(test_small_k_and_single_panel_rhs<float>());
CALL_SUBTEST_1(test_deep_tail_panels<float>());
CALL_SUBTEST_1(test_tiny_results<float>());
CALL_SUBTEST_1(test_fp_flags<float>());
CALL_SUBTEST_1(test_disjoint_parts<float>());
// double reaches the SME kernel and packers only with FEAT_SME_F64F64; the
// product sweep is meaningful either way, but the packed-layout tests name
// specializations that only exist when it is available.
CALL_SUBTEST_2(test_products<double>());
CALL_SUBTEST_2(test_conjugated_products<double>());
#ifdef EIGEN_VECTORIZE_SME_F64F64
CALL_SUBTEST_2(test_whilelt_operand_types<double>());
CALL_SUBTEST_2(test_packet_ops<double>());
CALL_SUBTEST_2(test_symm_pack<double>());
CALL_SUBTEST_2(test_pack_direct<double>());
CALL_SUBTEST_2(test_mapper_fallback<double>());
CALL_SUBTEST_2(test_neon_small_blocks<double>());
CALL_SUBTEST_2(test_direct_lhs<double>());
CALL_SUBTEST_2(test_small_k_and_single_panel_rhs<double>());
CALL_SUBTEST_2(test_deep_tail_panels<double>());
CALL_SUBTEST_2(test_tiny_results<double>());
CALL_SUBTEST_2(test_fp_flags<double>());
CALL_SUBTEST_2(test_disjoint_parts<double>());
#endif
CALL_SUBTEST_3(test_products<std::complex<float>>());
CALL_SUBTEST_3(test_conjugated_products<std::complex<float>>());
CALL_SUBTEST_3(test_symm_pack<std::complex<float>>());
CALL_SUBTEST_3(test_pack_direct<std::complex<float>>());
CALL_SUBTEST_3(test_mapper_fallback<std::complex<float>>());
CALL_SUBTEST_3(test_neon_small_blocks<std::complex<float>>());
CALL_SUBTEST_3(test_fp_flags<std::complex<float>>());
CALL_SUBTEST_3(test_disjoint_parts<std::complex<float>>());
CALL_SUBTEST_3((test_complex_alpha_range<float, ColMajor>()));
CALL_SUBTEST_3((test_complex_alpha_range<float, RowMajor>()));
// complex<double> accumulates into ZA.D tiles, so it needs FEAT_SME_F64F64
// exactly as double does.
CALL_SUBTEST_4(test_products<std::complex<double>>());
CALL_SUBTEST_4(test_conjugated_products<std::complex<double>>());
#ifdef EIGEN_VECTORIZE_SME_F64F64
CALL_SUBTEST_4(test_symm_pack<std::complex<double>>());
CALL_SUBTEST_4(test_pack_direct<std::complex<double>>());
CALL_SUBTEST_4(test_mapper_fallback<std::complex<double>>());
CALL_SUBTEST_4(test_neon_small_blocks<std::complex<double>>());
CALL_SUBTEST_4(test_fp_flags<std::complex<double>>());
CALL_SUBTEST_4(test_disjoint_parts<std::complex<double>>());
CALL_SUBTEST_4((test_complex_alpha_range<double, ColMajor>()));
CALL_SUBTEST_4((test_complex_alpha_range<double, RowMajor>()));
#endif
// A scalar type SME does not specialize, proving it still routes through the
// generic product path inside an SME build -- where packet traits, alignment
// and cache blocking all differ from a plain NEON build.
CALL_SUBTEST_5(product(Matrix<long double, Dynamic, Dynamic>(33, 17)));
}