blob: 5aec6e91a39a3fc3937a6c6b7140a3e58be60df5 [file]
// SPDX-FileCopyrightText: The Eigen Authors
// SPDX-License-Identifier: MPL-2.0
#ifndef EIGEN_SME_VECTOR_KERNELS_H
#define EIGEN_SME_VECTOR_KERNELS_H
// IWYU pragma: private
#include "../../InternalHeaderCheck.h"
namespace Eigen {
namespace internal {
#if EIGEN_COMP_CLANG
#define EIGEN_SME_VECTOR_UNROLL4 _Pragma("unroll")
#elif EIGEN_COMP_GNUC
#define EIGEN_SME_VECTOR_UNROLL4 _Pragma("GCC unroll 4")
#else
#define EIGEN_SME_VECTOR_UNROLL4
#endif
// FPCR.RMode == 0b10 (roundTowardNegative), the one mode where (+0) + (-0) is -0.
static EIGEN_ALWAYS_INLINE bool sme_rounds_toward_negative() {
std::uint64_t fpcr;
asm volatile("mrs %0, fpcr" : "=r"(fpcr));
return ((fpcr >> 22) & 3) == 2;
}
template <typename Scalar, typename Index>
__arm_new("za") __arm_locally_streaming
EIGEN_DONT_INLINE void sme_axpy(Index n, const Scalar* x, Scalar* y, Scalar alpha, Index prefix) {
using Traits = sme_packet_traits<Scalar>;
using Vec = typename Traits::type;
const Index lanes = Traits::size();
const auto pn = Traits::ptrue_c();
const auto a = pset1<Vec>(alpha);
Index i = 0;
for (; i < prefix; i += prefix - i < lanes ? prefix - i : lanes) {
auto active = Traits::whilelt(i, prefix);
pstoreu(active, y + i, pmadd(active, ploadu(active, x + i), a, ploadu(active, y + i)));
}
for (; i <= n - 16 * lanes; i += 16 * lanes) {
EIGEN_SME_VECTOR_UNROLL4
for (int k = 0; k < 4; ++k) {
auto xv = ploadu_x4(pn, x + i + k * 4 * lanes);
sme_write_za_vg1x4(k, ploadu_x4(pn, y + i + k * 4 * lanes));
sme_madd_za_vg1x4(k, xv, a);
}
EIGEN_SME_VECTOR_UNROLL4
for (int k = 0; k < 4; ++k) pstoreu_x4(pn, y + i + k * 4 * lanes, sme_read_za_vg1x4<Scalar>(k));
}
for (; i <= n - 4 * lanes; i += 4 * lanes) {
auto xv = ploadu_x4(pn, x + i);
sme_write_za_vg1x4(0, ploadu_x4(pn, y + i));
sme_madd_za_vg1x4(0, xv, a);
pstoreu_x4(pn, y + i, sme_read_za_vg1x4<Scalar>(0));
}
for (; i < n; i += n - i < lanes ? n - i : lanes) {
auto tail = Traits::whilelt(i, n);
pstoreu(tail, y + i, pmadd(tail, ploadu(tail, x + i), a, ploadu(tail, y + i)));
}
}
template <typename Scalar, typename Index>
__arm_new("za") __arm_locally_streaming EIGEN_DONT_INLINE Scalar sme_dot(Index n, const Scalar* x, const Scalar* y) {
using Traits = sme_packet_traits<Scalar>;
using Vec = typename Traits::type;
const Index lanes = Traits::size();
const auto pg = Traits::ptrue();
const auto pn = Traits::ptrue_c();
// -0 is the additive identity except under roundTowardNegative, so a zero sum gets the IEEE sign.
const auto negative_zero = pset1<Vec>(Scalar(-0.0));
EIGEN_SME_VECTOR_UNROLL4
for (int k = 0; k < 4; ++k)
sme_write_za_vg1x4(k, pcreate(negative_zero, negative_zero, negative_zero, negative_zero));
Index i = 0;
for (; i <= n - 16 * lanes; i += 16 * lanes) {
EIGEN_SME_VECTOR_UNROLL4
for (int k = 0; k < 4; ++k)
sme_madd_za_vg1x4(k, ploadu_x4(pn, x + i + k * 4 * lanes), ploadu_x4(pn, y + i + k * 4 * lanes));
}
for (; i <= n - 4 * lanes; i += 4 * lanes) sme_madd_za_vg1x4(0, ploadu_x4(pn, x + i), ploadu_x4(pn, y + i));
auto tail_accumulator = negative_zero;
for (; i < n; i += n - i < lanes ? n - i : lanes) {
auto tail = Traits::whilelt(i, n);
tail_accumulator = pmadd_m(tail, ploadu(tail, x + i), ploadu(tail, y + i), tail_accumulator);
}
auto sum = tail_accumulator;
EIGEN_SME_VECTOR_UNROLL4
for (int k = 0; k < 4; ++k) {
auto v = sme_read_za_vg1x4<Scalar>(k);
sum = padd(pg, sum, padd(pg, padd(pg, pget<0>(v), pget<1>(v)), padd(pg, pget<2>(v), pget<3>(v))));
}
return predux(pg, sum);
}
// y += alpha * ZA group k, updated in ZA like the accumulation.
template <typename Scalar, typename Vec>
static EIGEN_ALWAYS_INLINE void sme_gemv_update(unsigned int k, svcount_t active, Scalar* y,
Vec alpha) __arm_streaming __arm_inout("za") {
const auto sum = sme_read_za_vg1x4<Scalar>(k);
sme_write_za_vg1x4(k, ploadu_x4(active, y));
sme_madd_za_vg1x4(k, sum, alpha);
pstoreu_x4(active, y, sme_read_za_vg1x4<Scalar>(k));
}
template <typename Scalar, typename Index>
__arm_new("za") __arm_locally_streaming
EIGEN_DONT_INLINE void sme_gemv(Index rows, Index cols, const Scalar* a, Index stride, const Scalar* x, Scalar* y,
Scalar alpha, Index block_cols) {
using Traits = sme_packet_traits<Scalar>;
using Vec = typename Traits::type;
const Index lanes = Traits::size();
const auto scale = pset1<Vec>(alpha);
// Match the generic GEMV's scaled column batches to bound the unscaled sums.
for (Index first = 0; first < cols;) {
const Index end = first + (cols - first < block_cols ? cols - first : block_cols);
// Four predicated chunks per row block keep four independent FMLA chains, also in the final partial block.
// An inactive chunk addresses the block start and accesses no memory.
for (Index i = 0; i < rows;) {
const Index remaining = rows - i;
const auto p0 = Traits::whilelt_c4(i, rows), p1 = Traits::whilelt_c4(i + std::int64_t(4) * lanes, rows),
p2 = Traits::whilelt_c4(i + std::int64_t(8) * lanes, rows),
p3 = Traits::whilelt_c4(i + std::int64_t(12) * lanes, rows);
const Index o1 = remaining > 4 * lanes ? 4 * lanes : 0, o2 = remaining > 8 * lanes ? 8 * lanes : 0,
o3 = remaining > 12 * lanes ? 12 * lanes : 0;
svzero_za();
for (Index j = first; j < end; ++j) {
const auto b = pset1<Vec>(x[j]);
const Scalar* column = a + i + j * stride;
sme_madd_za_vg1x4(0, ploadu_x4(p0, column), b);
sme_madd_za_vg1x4(1, ploadu_x4(p1, column + o1), b);
sme_madd_za_vg1x4(2, ploadu_x4(p2, column + o2), b);
sme_madd_za_vg1x4(3, ploadu_x4(p3, column + o3), b);
}
sme_gemv_update(0, p0, y + i, scale);
sme_gemv_update(1, p1, y + i + o1, scale);
sme_gemv_update(2, p2, y + i + o2, scale);
sme_gemv_update(3, p3, y + i + o3, scale);
// Bound the final increment to avoid signed overflow with a 32-bit Index near its maximum.
i += remaining < 16 * lanes ? remaining : 16 * lanes;
}
first = end;
}
}
#undef EIGEN_SME_VECTOR_UNROLL4
template <typename Scalar>
struct sme_vector_scalar : false_type {};
template <>
struct sme_vector_scalar<float> : true_type {};
#ifdef EIGEN_VECTORIZE_SME_F64F64
template <>
struct sme_vector_scalar<double> : true_type {};
#endif
template <typename Xpr>
struct sme_vector_access : bool_constant<(sme_vector_scalar<typename traits<Xpr>::Scalar>::value &&
bool(Xpr::IsVectorAtCompileTime) && has_direct_access<Xpr>::value)> {};
template <typename Lhs, typename Rhs>
struct sme_dot_supported : bool_constant<sme_vector_access<Lhs>::value && sme_vector_access<Rhs>::value &&
is_same<typename traits<Lhs>::Scalar, typename traits<Rhs>::Scalar>::value> {};
// Fixed crossovers, fitted on Apple M4 (SVL 512): below them the NEON/SME memory handoff outweighs the
// streaming kernels. Core's cache estimates, including setCpuCacheSizes overrides, supply the others.
static constexpr std::size_t kSmeDotMinBytes = 16384; // per operand
static constexpr std::size_t kSmeAxpyMinBytes = 4096; // per operand
static constexpr Index kSmeGemvMinRows = 128;
static constexpr Index kSmeGemvMinCols = 4;
// A GEMV whose RHS reaches kSmeGemvWideRhsBytes needs only kSmeGemvWideMinBytes of matrix, not Core's L1 estimate.
static constexpr std::size_t kSmeGemvWideRhsBytes = 128;
static constexpr std::size_t kSmeGemvWideMinBytes = 32768;
// Keep cache queries out of callers so the small vector fallback can inline.
// Per operand, the L1 estimate (divided by L1Divisor) is the lower crossover and the L2 estimate the upper one.
template <typename Scalar, int L1Divisor = 1>
EIGEN_DONT_INLINE bool sme_vector_size_suitable(Index size) {
std::ptrdiff_t l1, l2, l3;
manage_caching_sizes(GetAction, &l1, &l2, &l3);
return l1 > 0 && l2 > 0 && size >= 0 &&
static_cast<std::size_t>(size) >= (static_cast<std::size_t>(l1) - 1) / (L1Divisor * sizeof(Scalar)) + 1 &&
static_cast<std::size_t>(size) <= static_cast<std::size_t>(l2) / sizeof(Scalar);
}
template <typename Lhs, typename Rhs>
struct default_inner_product_impl<Lhs, Rhs, true, std::enable_if_t<sme_dot_supported<Lhs, Rhs>::value>>
: default_inner_product_impl<Lhs, Rhs, true, false_type> {
using Base = default_inner_product_impl<Lhs, Rhs, true, false_type>;
using Scalar = typename traits<Lhs>::Scalar;
static EIGEN_STRONG_INLINE Scalar run(const MatrixBase<Lhs>& lhs, const MatrixBase<Rhs>& rhs) {
inner_product_assert<Lhs, Rhs>::run(lhs.derived(), rhs.derived());
// DOT has no streaming stores for a following NEON consumer: half L1 per operand suffices.
if (lhs.size() >= Index(kSmeDotMinBytes / sizeof(Scalar)) && sme_vector_size_suitable<Scalar, 2>(lhs.size()) &&
lhs.innerStride() == 1 && rhs.innerStride() == 1) {
sme_fpsr_guard status;
const Scalar result = sme_dot(lhs.size(), lhs.derived().data(), rhs.derived().data());
// Under roundTowardNegative the -0 seeds turn +0 sums into -0.
if (result != Scalar(0) || !sme_rounds_toward_negative()) return result;
}
return Base::run(lhs, rhs);
}
};
template <typename Dst, typename Src>
EIGEN_STRONG_INLINE bool sme_try_axpy(Dst& dst, const Src& src, typename traits<Src>::Scalar alpha) {
using Scalar = typename traits<Src>::Scalar;
eigen_assert(dst.rows() == src.rows() && dst.cols() == src.cols());
if (dst.size() < Index(kSmeAxpyMinBytes / sizeof(Scalar)) || !sme_vector_size_suitable<Scalar>(dst.size()) ||
dst.innerStride() != 1 || src.innerStride() != 1)
return false;
const std::uintptr_t dst_address = reinterpret_cast<std::uintptr_t>(dst.data());
const std::uintptr_t src_address = reinterpret_cast<std::uintptr_t>(src.data());
const std::uintptr_t distance = dst_address > src_address ? dst_address - src_address : src_address - dst_address;
// Exact aliasing is coefficient-wise; partial overlap must retain the default traversal.
if (distance != 0 && distance / sizeof(Scalar) < static_cast<std::uintptr_t>(dst.size())) return false;
// Align streaming stores to 64 bytes without changing Eigen's allocation alignment.
sme_fpsr_guard status;
sme_axpy(dst.size(), src.data(), dst.data(), alpha, first_aligned<64>(dst.data(), dst.size()));
return true;
}
template <typename Dst, typename Scalar, typename Lhs, typename Rhs>
struct Assignment<
Dst, CwiseBinaryOp<scalar_product_op<Scalar, Scalar>, Lhs, Rhs>, add_assign_op<Scalar, Scalar>, Dense2Dense,
std::enable_if_t<sme_vector_access<Dst>::value &&
(sme_vector_access<Lhs>::value || sme_vector_access<Rhs>::value) &&
blas_traits<CwiseBinaryOp<scalar_product_op<Scalar, Scalar>, Lhs, Rhs>>::HasScalarFactor &&
is_same<Scalar, typename traits<Dst>::Scalar>::value>> {
using Source = CwiseBinaryOp<scalar_product_op<Scalar, Scalar>, Lhs, Rhs>;
using BlasTraits = blas_traits<Source>;
static EIGEN_STRONG_INLINE void run(Dst& dst, const Source& src, const add_assign_op<Scalar, Scalar>& func) {
// A direct operand restricts extraction to one scale, preserving nested products' evaluation order.
if (!sme_try_axpy(dst, BlasTraits::extract(src), BlasTraits::extractScalarFactor(src)))
Assignment<Dst, Source, add_assign_op<Scalar, Scalar>, Dense2Dense, false_type>::run(dst, src, func);
}
};
#ifndef EIGEN_USE_BLAS
template <typename Scalar, typename Index>
EIGEN_STRONG_INLINE bool sme_gemv_size_suitable(Index rows, Index cols) {
if (rows < Index(kSmeGemvMinRows) || cols < Index(kSmeGemvMinCols)) return false;
if (cols >= Index(kSmeGemvWideRhsBytes / sizeof(Scalar)) &&
cols > Index(kSmeGemvWideMinBytes / sizeof(Scalar) - 1) / rows)
return true;
// Thin products amortize the NEON/SME handoff once the matrix reaches Core's L1 estimate.
const std::ptrdiff_t l1 = l1CacheSize();
return l1 > 0 && static_cast<std::size_t>(cols) >
(static_cast<std::size_t>(l1) - 1) / sizeof(Scalar) / static_cast<std::size_t>(rows);
}
#define EIGEN_SME_GEMV_SPECIALIZATION(Scalar) \
template <typename Index, bool ConjugateLhs, bool ConjugateRhs> \
struct general_matrix_vector_product<Index, Scalar, const_blas_data_mapper<Scalar, Index, ColMajor>, ColMajor, \
ConjugateLhs, Scalar, const_blas_data_mapper<Scalar, Index, RowMajor>, \
ConjugateRhs, Specialized> { \
static EIGEN_STRONG_INLINE void run(Index rows, Index cols, \
const const_blas_data_mapper<Scalar, Index, ColMajor>& lhs, \
const const_blas_data_mapper<Scalar, Index, RowMajor>& rhs, Scalar* res, \
Index resIncr, Scalar alpha) { \
if (sme_gemv_size_suitable<Scalar>(rows, cols) && rhs.stride() == 1 && resIncr == 1) { \
const std::ptrdiff_t l1 = l1CacheSize(); \
const Index block_cols = cols < 128 ? cols \
: (l1 > 0 && static_cast<std::size_t>(lhs.stride()) <= \
(static_cast<std::size_t>(l1) - 1) / sizeof(Scalar) \
? Index(16) \
: Index(4)); \
/* BLAS contract: alpha == 0 leaves the result unchanged. */ \
if (alpha == Scalar(0)) return; \
sme_fpsr_guard status; \
sme_gemv(rows, cols, lhs.data(), lhs.stride(), rhs.data(), res, alpha, block_cols); \
} else { \
general_matrix_vector_product<Index, Scalar, const_blas_data_mapper<Scalar, Index, ColMajor>, ColMajor, \
ConjugateLhs, Scalar, const_blas_data_mapper<Scalar, Index, RowMajor>, \
ConjugateRhs, BuiltIn>::run(rows, cols, lhs, rhs, res, resIncr, alpha); \
} \
} \
};
EIGEN_SME_GEMV_SPECIALIZATION(float)
#ifdef EIGEN_VECTORIZE_SME_F64F64
EIGEN_SME_GEMV_SPECIALIZATION(double)
#endif
#undef EIGEN_SME_GEMV_SPECIALIZATION
#endif
} // namespace internal
} // namespace Eigen
#endif