blob: a5a5815b0d697063b3a98fbd0d93ab2f64c2ae34 [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
#ifndef EIGEN_SME_GENERALBLOCKPANELKERNEL_H
#define EIGEN_SME_GENERALBLOCKPANELKERNEL_H
// IWYU pragma: private
#include "../../InternalHeaderCheck.h"
#include <arm_sme.h>
namespace Eigen {
namespace internal {
// ---------------------------------------------------------------------------
// Streaming vector length and tile geometry.
//
// The micro-kernel is organised around a logical mr x nr output block, packed
// depth-major (mr contiguous scalars per depth step). Those dimensions are
// compile-time constants: they feed gebp_traits (cache blocking) and the
// packers, none of which can depend on a runtime value.
//
// The *physical* tiling of that block onto ZA tiles, on the other hand, is
// driven by the runtime streaming vector length. A ZA tile of Scalar is
// svl x svl, where svl is the number of Scalars in a streaming vector
// (svcntsw() for fp32, svcntsd() for fp64). The block is covered by up to a
// 2x2 grid of svl x svl tiles, iterated in sub-block passes when the grid is
// smaller than the block (and predicated down to it when larger).
//
// fp32 uses the 4 ZA.S tiles, so the 2x2 grid is all of ZA. fp64 uses ZA.D, of
// which there are 8, and deliberately leaves tiles 4-7 idle: a 2x2 grid loads 2
// packed vectors per side per depth step to feed 4 FMOPAs, i.e. 64 bytes of
// packed panel per FMOPA at either element width, and FMOPA issues at the same
// rate for both. A 2x4 grid over all eight needs a quarter less panel traffic
// per FMOPA and still measures 0.92-1.00x of the 2x2 on Apple M4, so the wider
// block is not worth its L1 footprint.
//
// A complex accumulator is a *pair* of tiles holding its real and imaginary
// halves, since FMOPA only takes real operands, so complex<float> gets a 1x2
// grid of pairs out of ZA.S and complex<double> a 2x2 grid out of ZA.D. The
// packed panels are correspondingly split -- one depth step holds the real
// parts of the panel width then the imaginary ones -- so the four real outer
// products a complex one expands to reuse both operands and the panel traffic
// per FMOPA halves relative to the real kernels.
//
// This translation unit must be built without -msve-vector-bits (scalable/VLA
// mode); see the guard in ConfigureVectorization.h for the rationale.
// Everything below derives lane counts/predicates from the runtime svl; when a
// block matches the tile grid exactly, the micro-kernel additionally switches
// to a hand-scheduled multi-vector-load loop (see sme_process).
// ---------------------------------------------------------------------------
// The streaming vector width the block sizes are chosen at: 512 bits, where a
// vector holds 64 / sizeof(element) elements. Other SVLs tile the block at
// runtime. If a future SVL ever justifies a larger block, this is the only
// knob -- but don't grow it speculatively, a doubled block measures slower at
// SVL=512.
static constexpr int kSmeDesignVectorBytes = 64;
// Logical micro-kernel block (LHS/RHS panel widths), as a grid of ZA tiles each
// svl x svl elements of the ZA element width -- Scalar itself for a real
// scalar, its real part for a complex one, whose halves accumulate into
// separate tiles -- a full 2x2 grid for real scalars.
// A complex accumulator takes a tile pair, so complex<float> gets half the grid
// cells of float; the grid stays two cells wide, which measures 1.2-1.9x a
// two-cell-tall one below 128 on Apple M4 and matches it above.
template <typename Scalar>
struct sme_block {
static constexpr int kGridRows = 2;
static constexpr int kGridCols = 2;
static constexpr int mr = kGridRows * kSmeDesignVectorBytes / int(sizeof(Scalar));
static constexpr int nr = kGridCols * kSmeDesignVectorBytes / int(sizeof(Scalar));
};
template <typename RealScalar>
struct sme_block<std::complex<RealScalar>> {
static constexpr int kGridCols = 2;
static constexpr int kGridRows = sme_tile_count<RealScalar>::value / (2 * kGridCols);
static constexpr int mr = kGridRows * kSmeDesignVectorBytes / int(sizeof(RealScalar));
static constexpr int nr = kGridCols * kSmeDesignVectorBytes / int(sizeof(RealScalar));
};
static constexpr int kSmeMr = sme_block<float>::mr;
static constexpr int kSmeNr = sme_block<float>::nr;
static constexpr int kSmeMrC = sme_block<std::complex<float>>::mr;
static constexpr int kSmeNrC = sme_block<std::complex<float>>::nr;
#ifdef EIGEN_VECTORIZE_SME_F64F64
static constexpr int kSmeMrD = sme_block<double>::mr;
static constexpr int kSmeNrD = sme_block<double>::nr;
static constexpr int kSmeMrCD = sme_block<std::complex<double>>::mr;
static constexpr int kSmeNrCD = sme_block<std::complex<double>>::nr;
#endif
// ---------------------------------------------------------------------------
// Packed panel layout and the primitives that produce it.
//
// A packed panel is depth-major. For a real element type one depth step holds
// `width` contiguous scalars; for a complex one it holds the `width` real parts
// followed by the `width` imaginary parts, so the micro-kernel feeds a ZA tile
// pair from two contiguous vector loads and never deinterleaves inside the
// depth loop. Either way a width x depth panel occupies width * depth Scalars,
// which is what the GEMM driver allocates.
//
// `Conjugate` negates the imaginary half. Conjugation is the identity on real
// scalars, so the real overloads ignore it -- and Conjugate=true instantiations
// do reach them, from the SYMM above-diagonal transposed pack.
// ---------------------------------------------------------------------------
// Copy `width` contiguous source columns per depth step into a depth-major
// packed panel of width `width`, for the depth sub-range [k0, k1). Both dst and
// src are indexed by the absolute depth index k (dst[k*width+off],
// src[k*src_stride+off]); the caller offsets `src` to the region's column base
// and `dst` to the panel base. Generalised over the runtime svl: the panel is
// covered in svl-wide column chunks, each streamed over the depth sub-range.
// The chunk loop is outermost so each chunk's predicate is computed once instead
// of per depth step (the runtime chunk count keeps the compiler from hoisting it
// on its own). The symm packers reuse this for the diagonal-split direct/
// transposed regions (a contiguous depth sub-range at a depth offset).
template <bool Conjugate, typename Scalar, typename Index>
static EIGEN_ALWAYS_INLINE void sve_copy_panel_range(Scalar* EIGEN_RESTRICT dst, const Scalar* EIGEN_RESTRICT src,
Index src_stride, Index k0, Index k1, int width) __arm_streaming {
using Traits = sme_packet_traits<Scalar>;
const int svl = Traits::size();
if (width == 2 * svl) {
// Full panel: one two-vector load and store per depth step, four steps in flight.
const svcount_t pn = Traits::ptrue_c();
Index k = k0;
for (; k + 4 <= k1; k += 4) {
const auto v0 = ploadu_x2(pn, &src[k * src_stride]);
const auto v1 = ploadu_x2(pn, &src[(k + 1) * src_stride]);
const auto v2 = ploadu_x2(pn, &src[(k + 2) * src_stride]);
const auto v3 = ploadu_x2(pn, &src[(k + 3) * src_stride]);
pstoreu_x2(pn, &dst[k * width], v0);
pstoreu_x2(pn, &dst[(k + 1) * width], v1);
pstoreu_x2(pn, &dst[(k + 2) * width], v2);
pstoreu_x2(pn, &dst[(k + 3) * width], v3);
}
for (; k < k1; ++k) pstoreu_x2(pn, &dst[k * width], ploadu_x2(pn, &src[k * src_stride]));
return;
}
for (int off = 0; off < width; off += svl) {
const svbool_t pred = sme_packet_traits<Scalar>::whilelt(off, width);
for (Index k = k0; k < k1; ++k) {
pstoreu(pred, &dst[k * width + off], ploadu(pred, &src[k * src_stride + off]));
}
}
}
// Four full 2*svl-wide panels from one 8*svl-wide source strip (panel p at dst + p * panel_stride): two four-vector
// loads per depth step, and the core prefetches the strip into L2 four steps ahead.
template <typename Scalar, typename Index>
static EIGEN_ALWAYS_INLINE void sve_copy_panel_quad(Scalar* EIGEN_RESTRICT dst, Index panel_stride,
const Scalar* EIGEN_RESTRICT src, Index src_stride,
Index depth) __arm_streaming {
using Traits = sme_packet_traits<Scalar>;
const int w = 2 * Traits::size();
const svcount_t pn = Traits::ptrue_c();
const int strip_bytes = 4 * w * int(sizeof(Scalar));
for (Index k = 0; k < depth; ++k) {
const Scalar* c = src + k * src_stride;
if (k + 4 < depth)
for (int l = 0; l < strip_bytes; l += 128)
__builtin_prefetch(reinterpret_cast<const char*>(c + 4 * src_stride) + l, 0, 2);
const auto a = ploadu_x4(pn, c), b = ploadu_x4(pn, c + 2 * w);
pstoreu_x2(pn, dst + k * w, pcreate(pget<0>(a), pget<1>(a)));
pstoreu_x2(pn, dst + panel_stride + k * w, pcreate(pget<2>(a), pget<3>(a)));
pstoreu_x2(pn, dst + 2 * panel_stride + k * w, pcreate(pget<0>(b), pget<1>(b)));
pstoreu_x2(pn, dst + 3 * panel_stride + k * w, pcreate(pget<2>(b), pget<3>(b)));
}
}
// Packs the leading groups of four full MR-wide panels with sve_copy_panel_quad; returns the rows it covered.
// Complex panels keep the per-panel path (their packed layout splits real and imaginary halves).
template <bool PanelMode, typename Scalar, typename Index>
static EIGEN_ALWAYS_INLINE Index sme_pack_quad_panels(Scalar* dst_base, const Scalar* EIGEN_RESTRICT src,
Index src_stride, Index depth, Index rows, Index mr,
Index dst_stride, Index dst_offset) __arm_streaming {
if (mr != 2 * sme_packet_traits<Scalar>::size()) return 0;
const Index panel_stride = PanelMode ? mr * dst_stride : mr * depth;
Index i = 0;
for (; i + 4 * mr <= rows; i += 4 * mr) {
Scalar* dst_panel = PanelMode ? dst_base + i * dst_stride + dst_offset * mr : dst_base + i * depth;
sve_copy_panel_quad(dst_panel, panel_stride, src + i, src_stride, depth);
}
return i;
}
template <bool PanelMode, typename RealScalar, typename Index>
static EIGEN_ALWAYS_INLINE Index sme_pack_quad_panels(std::complex<RealScalar>*, const std::complex<RealScalar>*, Index,
Index, Index, Index, Index, Index) __arm_streaming {
return 0;
}
// Complex overload: UZP1/UZP2 split the source's interleaved pairs into the two
// halves of the packed depth step. A chunk of w complex elements spans 2*w
// interleaved reals, hence a pair of source predicates; the upper one is empty
// whenever 2*w fits in one vector.
template <bool Conjugate, typename RealScalar, typename Index>
static EIGEN_ALWAYS_INLINE void sve_copy_panel_range(std::complex<RealScalar>* EIGEN_RESTRICT dst,
const std::complex<RealScalar>* EIGEN_RESTRICT src,
Index src_stride, Index k0, Index k1, int width) __arm_streaming {
using Traits = sme_packet_traits<RealScalar>;
using Vec = typename Traits::type;
RealScalar* EIGEN_RESTRICT rdst = reinterpret_cast<RealScalar*>(dst);
const RealScalar* EIGEN_RESTRICT rsrc = reinterpret_cast<const RealScalar*>(src);
const int svl = Traits::size();
const Index step = Index(2 * width);
for (int off = 0; off < width; off += svl) {
const int w = width - off;
const int lanes = 2 * w;
const svbool_t pg_w = Traits::whilelt(off, width);
const svbool_t pg_lo = Traits::whilelt(0, lanes);
const svbool_t pg_hi = Traits::whilelt(svl, lanes);
for (Index k = k0; k < k1; ++k) {
const RealScalar* p = rsrc + Index(2) * (k * src_stride + Index(off));
const Vec v_lo = ploadu(pg_lo, p);
// pg_hi is all-false when 2*w fits in one vector, and an inactive lane
// makes no memory access -- but p + svl may still be past the source, so
// the address is formed through sme_offset.
const Vec v_hi = ploadu(pg_hi, sme_offset(p, svl));
Vec im = puzp2(v_lo, v_hi);
EIGEN_IF_CONSTEXPR (Conjugate) {
im = pnegate(pg_w, im);
}
pstoreu(pg_w, &rdst[k * step + Index(off)], puzp1(v_lo, v_hi));
pstoreu(pg_w, &rdst[k * step + Index(width + off)], im);
}
}
}
// Copy the full depth [0, depth): thin wrapper used by the (non-symm) gemm
// packers, which always pack a whole panel.
template <bool Conjugate, typename Scalar, typename Index>
static EIGEN_ALWAYS_INLINE void sve_copy_panel(Scalar* EIGEN_RESTRICT dst, const Scalar* EIGEN_RESTRICT src,
Index src_stride, Index depth, int width) __arm_streaming {
sve_copy_panel_range<Conjugate>(dst, src, src_stride, Index(0), depth, width);
}
// Full 2*svl-wide panel through all four tiles, four rows or depth steps per ZA move, source prefetched into L2
// two steps ahead; returns the first depth index of the tail (< 2*svl) it leaves to the caller.
template <typename RealScalar, typename Index>
static EIGEN_ALWAYS_INLINE Index sme_transpose_pack_pair(RealScalar* EIGEN_RESTRICT dst,
const RealScalar* EIGEN_RESTRICT src, Index src_stride,
Index k0, Index k1) __arm_streaming __arm_inout("za") {
using Traits = sme_packet_traits<RealScalar>;
const int svl = Traits::size(), w = 2 * svl;
const svcount_t pn = Traits::ptrue_c();
Index k = k0;
for (; k + w <= k1; k += w) {
const bool pf = k + 2 * w <= k1;
for (int r = 0; r < svl; r += 4) {
const RealScalar* p = src + Index(r) * src_stride + k;
const RealScalar* q = p + Index(svl) * src_stride;
if (pf)
for (int u = 0; u < 4; ++u) {
__builtin_prefetch(p + u * src_stride + 2 * w, 0, 2);
__builtin_prefetch(q + u * src_stride + 2 * w, 0, 2);
}
const auto a0 = ploadu_x2(pn, p), a1 = ploadu_x2(pn, p + src_stride), a2 = ploadu_x2(pn, p + 2 * src_stride),
a3 = ploadu_x2(pn, p + 3 * src_stride);
sme_write_hor_za_vg4<0>(uint32_t(r), pget<0>(a0), pget<0>(a1), pget<0>(a2), pget<0>(a3));
sme_write_hor_za_vg4<1>(uint32_t(r), pget<1>(a0), pget<1>(a1), pget<1>(a2), pget<1>(a3));
const auto b0 = ploadu_x2(pn, q), b1 = ploadu_x2(pn, q + src_stride), b2 = ploadu_x2(pn, q + 2 * src_stride),
b3 = ploadu_x2(pn, q + 3 * src_stride);
sme_write_hor_za_vg4<2>(uint32_t(r), pget<0>(b0), pget<0>(b1), pget<0>(b2), pget<0>(b3));
sme_write_hor_za_vg4<3>(uint32_t(r), pget<1>(b0), pget<1>(b1), pget<1>(b2), pget<1>(b3));
}
for (int c = 0; c < svl; c += 4) {
const auto t0 = sme_read_ver_za_vg4<0, RealScalar>(uint32_t(c)),
t2 = sme_read_ver_za_vg4<2, RealScalar>(uint32_t(c));
const auto t1 = sme_read_ver_za_vg4<1, RealScalar>(uint32_t(c)),
t3 = sme_read_ver_za_vg4<3, RealScalar>(uint32_t(c));
RealScalar* d0 = dst + (k + c) * w;
RealScalar* d1 = d0 + Index(svl) * w;
pstoreu_x2(pn, d0, pcreate(pget<0>(t0), pget<0>(t2)));
pstoreu_x2(pn, d0 + w, pcreate(pget<1>(t0), pget<1>(t2)));
pstoreu_x2(pn, d0 + 2 * w, pcreate(pget<2>(t0), pget<2>(t2)));
pstoreu_x2(pn, d0 + 3 * w, pcreate(pget<3>(t0), pget<3>(t2)));
pstoreu_x2(pn, d1, pcreate(pget<0>(t1), pget<0>(t3)));
pstoreu_x2(pn, d1 + w, pcreate(pget<1>(t1), pget<1>(t3)));
pstoreu_x2(pn, d1 + 2 * w, pcreate(pget<2>(t1), pget<2>(t3)));
pstoreu_x2(pn, d1 + 3 * w, pcreate(pget<3>(t1), pget<3>(t3)));
}
}
return k;
}
// A row of a partial panel, or zeros past its last row (those slices are never stored).
template <typename RealScalar>
static EIGEN_ALWAYS_INLINE typename sme_packet_traits<RealScalar>::type_x2 sme_row_or_zero(
const RealScalar* p, bool valid, svcount_t pn, typename sme_packet_traits<RealScalar>::type zero) __arm_streaming {
return valid ? ploadu_x2(pn, p) : pcreate(zero, zero);
}
// Partial panel (width < 2*svl) through the tiles as sme_transpose_pack_pair does, rows past the width as zeros and
// predicated stores of `width` scalars per depth step; returns the first depth index of the tail it leaves.
template <typename RealScalar, typename Index>
static EIGEN_ALWAYS_INLINE Index sme_transpose_pack_partial(RealScalar* EIGEN_RESTRICT dst,
const RealScalar* EIGEN_RESTRICT src, Index src_stride,
Index k0, Index k1,
int width) __arm_streaming __arm_inout("za") {
using Traits = sme_packet_traits<RealScalar>;
using Vec = typename Traits::type;
const int svl = Traits::size(), w2 = 2 * svl;
const svcount_t pn = Traits::ptrue_c();
const Vec zero = pset1<Vec>(RealScalar(0));
const int rows_lo = sme_min(width, svl), rows_hi = width - rows_lo;
const svbool_t pg_lo = Traits::whilelt(0, rows_lo), pg_hi = Traits::whilelt(0, rows_hi);
const svbool_t pg_two = Traits::whilelt(0, 2 * width);
Index k = k0;
for (; k + w2 <= k1; k += w2) {
for (int r = 0; r < rows_lo; r += 4) {
const RealScalar* q = src + Index(r) * src_stride + k;
const auto a0 = sme_row_or_zero<RealScalar>(q, r < rows_lo, pn, zero);
const auto a1 = sme_row_or_zero<RealScalar>(q + src_stride, r + 1 < rows_lo, pn, zero);
const auto a2 = sme_row_or_zero<RealScalar>(q + 2 * src_stride, r + 2 < rows_lo, pn, zero);
const auto a3 = sme_row_or_zero<RealScalar>(q + 3 * src_stride, r + 3 < rows_lo, pn, zero);
sme_write_hor_za_vg4<0>(uint32_t(r), pget<0>(a0), pget<0>(a1), pget<0>(a2), pget<0>(a3));
sme_write_hor_za_vg4<1>(uint32_t(r), pget<1>(a0), pget<1>(a1), pget<1>(a2), pget<1>(a3));
}
for (int r = 0; r < rows_hi; r += 4) {
const RealScalar* q = src + Index(svl + r) * src_stride + k;
const auto a0 = sme_row_or_zero<RealScalar>(q, r < rows_hi, pn, zero);
const auto a1 = sme_row_or_zero<RealScalar>(q + src_stride, r + 1 < rows_hi, pn, zero);
const auto a2 = sme_row_or_zero<RealScalar>(q + 2 * src_stride, r + 2 < rows_hi, pn, zero);
const auto a3 = sme_row_or_zero<RealScalar>(q + 3 * src_stride, r + 3 < rows_hi, pn, zero);
sme_write_hor_za_vg4<2>(uint32_t(r), pget<0>(a0), pget<0>(a1), pget<0>(a2), pget<0>(a3));
sme_write_hor_za_vg4<3>(uint32_t(r), pget<1>(a0), pget<1>(a1), pget<1>(a2), pget<1>(a3));
}
if (2 * width <= svl) {
// Two depth steps per store: half as many stores, and full lines at width svl/2.
for (int c = 0; c < svl; c += 4) {
const auto t0 = sme_read_ver_za_vg4<0, RealScalar>(uint32_t(c)),
t1 = sme_read_ver_za_vg4<1, RealScalar>(uint32_t(c));
RealScalar* d0 = dst + (k + c) * width;
RealScalar* d1 = d0 + Index(svl) * width;
pstoreu(pg_two, d0, psplice(pg_lo, pget<0>(t0), pget<1>(t0)));
pstoreu(pg_two, d0 + 2 * width, psplice(pg_lo, pget<2>(t0), pget<3>(t0)));
pstoreu(pg_two, d1, psplice(pg_lo, pget<0>(t1), pget<1>(t1)));
pstoreu(pg_two, d1 + 2 * width, psplice(pg_lo, pget<2>(t1), pget<3>(t1)));
}
continue;
}
for (int c = 0; c < svl; c += 4) {
const auto t0 = sme_read_ver_za_vg4<0, RealScalar>(uint32_t(c)),
t1 = sme_read_ver_za_vg4<1, RealScalar>(uint32_t(c));
RealScalar* d0 = dst + (k + c) * width;
RealScalar* d1 = d0 + Index(svl) * width;
pstoreu(pg_lo, d0, pget<0>(t0));
pstoreu(pg_lo, d0 + width, pget<1>(t0));
pstoreu(pg_lo, d0 + 2 * width, pget<2>(t0));
pstoreu(pg_lo, d0 + 3 * width, pget<3>(t0));
pstoreu(pg_lo, d1, pget<0>(t1));
pstoreu(pg_lo, d1 + width, pget<1>(t1));
pstoreu(pg_lo, d1 + 2 * width, pget<2>(t1));
pstoreu(pg_lo, d1 + 3 * width, pget<3>(t1));
if (rows_hi > 0) {
const auto t2 = sme_read_ver_za_vg4<2, RealScalar>(uint32_t(c)),
t3 = sme_read_ver_za_vg4<3, RealScalar>(uint32_t(c));
pstoreu(pg_hi, d0 + svl, pget<0>(t2));
pstoreu(pg_hi, d0 + width + svl, pget<1>(t2));
pstoreu(pg_hi, d0 + 2 * width + svl, pget<2>(t2));
pstoreu(pg_hi, d0 + 3 * width + svl, pget<3>(t2));
pstoreu(pg_hi, d1 + svl, pget<0>(t3));
pstoreu(pg_hi, d1 + width + svl, pget<1>(t3));
pstoreu(pg_hi, d1 + 2 * width + svl, pget<2>(t3));
pstoreu(pg_hi, d1 + 3 * width + svl, pget<3>(t3));
}
}
}
return k;
}
// Transpose-pack `width` source rows into depth-major packed output using ZA's
// 2D store as a free transpose, for the depth sub-range [k0, k1): a svl x svl
// block of source (svl rows x svl depth) is loaded as horizontal ZA slices,
// then read back as vertical slices, which emits it depth-major. Row-groups of
// svl rows are processed two at a time through ZA tiles 0 and 1: ZA is not
// renamed, so a single tile would stall every load pass on the previous read
// pass (write-after-read); two tiles in flight keep the phases independent.
// Trailing row-groups (when width is not a multiple of 2*svl) use tile 0 with
// predicated rows -- which is also what a panel narrower than 2*svl gets in
// full: complex<float> has mr = svl at SVL=512, so its LHS panels take the
// single-tile path and do not get the write-after-read overlap described above.
// Widening the gate would need the pairing to run over depth instead of rows.
// Both dst and src are indexed by the absolute depth index k:
// dst[k*width + r] = src[r*src_stride + k], k in [k0,k1), r in [0,width).
// The symm packers reuse this for the diagonal-split transposed/direct regions
// (a depth sub-range at a depth offset, with a tail-panel width < mr).
//
// NegateOddRows negates every odd output depth row. That is how the complex
// overload below conjugates: in the real view of a complex panel those rows are
// exactly the imaginary halves.
template <bool NegateOddRows, typename RealScalar, typename Index>
static EIGEN_ALWAYS_INLINE void sme_transpose_pack_real(RealScalar* EIGEN_RESTRICT dst,
const RealScalar* EIGEN_RESTRICT src, Index src_stride,
Index k0, Index k1,
int width) __arm_streaming __arm_inout("za") {
using Traits = sme_packet_traits<RealScalar>;
using Vec = typename Traits::type;
const Vec zero = pset1<Vec>(RealScalar(0));
const svbool_t pg_all = Traits::ptrue();
const int svl = Traits::size();
Index k = k0;
EIGEN_IF_CONSTEXPR (!NegateOddRows) {
// Short ranges keep the single-tile path: the four-slice moves only pay off over several fills, and need a tile
// of at least four slices.
if (svl >= 4 && width == 2 * svl && k1 - k0 >= Index(8 * svl))
k = sme_transpose_pack_pair(dst, src, src_stride, k0, k1);
else if (svl >= 4 && width < 2 * svl && k1 - k0 >= Index(8 * svl))
k = sme_transpose_pack_partial(dst, src, src_stride, k0, k1, width);
}
for (; k < k1; k += svl) {
const int dk = static_cast<int>(sme_min(k1 - k, Index(svl)));
const svbool_t pg_d = Traits::whilelt(k, k1);
int r0 = 0;
// Pairs of full row-groups: tiles 0 and 1 in flight.
for (; r0 + 2 * svl <= width; r0 += 2 * svl) {
for (int r = 0; r < svl; ++r) {
sme_ld1_hor_za<0>(uint32_t(r), pg_d, &src[(r0 + r) * src_stride + k]);
sme_ld1_hor_za<1>(uint32_t(r), pg_d, &src[(r0 + svl + r) * src_stride + k]);
}
for (int c = 0; c < dk; ++c) {
Vec v0 = sme_read_ver_za<0>(zero, pg_all, uint32_t(c));
Vec v1 = sme_read_ver_za<1>(zero, pg_all, uint32_t(c));
EIGEN_IF_CONSTEXPR (NegateOddRows) {
if (((k + Index(c)) & Index(1)) != Index(0)) {
v0 = pnegate(pg_all, v0);
v1 = pnegate(pg_all, v1);
}
}
pstoreu(pg_all, &dst[(k + c) * width + r0], v0);
pstoreu(pg_all, &dst[(k + c) * width + r0 + svl], v1);
}
}
// Trailing row-groups (at most two svl-wide passes remain, since the pair
// loop consumed all multiples of 2*svl): predicate down to the remaining
// rows. A single `if` would drop rows when a tail width lands in
// (svl, 2*svl); a loop handles any leftover.
for (; r0 < width; r0 += svl) {
const int rg = sme_min(width - r0, svl);
const svbool_t pg_r = Traits::whilelt(r0, width);
for (int r = 0; r < rg; ++r) {
sme_ld1_hor_za<0>(uint32_t(r), pg_d, &src[(r0 + r) * src_stride + k]);
}
for (int c = 0; c < dk; ++c) {
Vec v0 = sme_read_ver_za<0>(zero, pg_r, uint32_t(c));
EIGEN_IF_CONSTEXPR (NegateOddRows) {
if (((k + Index(c)) & Index(1)) != Index(0)) v0 = pnegate(pg_r, v0);
}
pstoreu(pg_r, &dst[(k + c) * width + r0], v0);
}
}
}
}
template <bool Conjugate, typename Scalar, typename Index>
static EIGEN_ALWAYS_INLINE void sme_transpose_pack_range(Scalar* EIGEN_RESTRICT dst, const Scalar* EIGEN_RESTRICT src,
Index src_stride, Index k0, Index k1,
int width) __arm_streaming __arm_inout("za") {
sme_transpose_pack_real<false>(dst, src, src_stride, k0, k1, width);
}
// Complex overload. A ColMajor (RowMajor) complex operand is a ColMajor
// (RowMajor) real one of twice the depth and twice the stride, and transposing
// that real view already emits the split layout: real-view depth 2k lands at
// packed offset k*(2*width) and depth 2k+1 at k*(2*width) + width, the real and
// imaginary halves of packed depth step k.
template <bool Conjugate, typename RealScalar, typename Index>
static EIGEN_ALWAYS_INLINE void sme_transpose_pack_range(std::complex<RealScalar>* EIGEN_RESTRICT dst,
const std::complex<RealScalar>* EIGEN_RESTRICT src,
Index src_stride, Index k0, Index k1,
int width) __arm_streaming __arm_inout("za") {
sme_transpose_pack_real<Conjugate>(reinterpret_cast<RealScalar*>(dst), reinterpret_cast<const RealScalar*>(src),
Index(2) * src_stride, Index(2) * k0, Index(2) * k1, width);
}
// Transpose-pack a whole `width`-wide panel over the full depth [0, depth):
// thin wrapper used by the (non-symm) gemm packers.
template <bool Conjugate, typename Scalar, typename Index>
static EIGEN_ALWAYS_INLINE void sme_transpose_pack(Scalar* EIGEN_RESTRICT dst, const Scalar* EIGEN_RESTRICT src,
Index src_stride, Index depth,
int width) __arm_streaming __arm_inout("za") {
sme_transpose_pack_range<Conjugate>(dst, src, src_stride, Index(0), depth, width);
}
// Transposing copy for a panel narrower than the pack width:
// dst_panel[k*tail + i] = src[i*src_stride + k].
// Kept outside the caller's __arm_locally_streaming region: it needs neither SVE
// nor ZA, and streaming mode runs scalar floating-point ~40x slower on Apple M4.
// Outside it the source rows are contiguous in k, so PacketSize of them
// transpose in register as in sme_pack_rhs_fallback; a product with cols < nr is
// packed entirely here. NegateOddRows is as in sme_transpose_pack_real.
template <bool NegateOddRows, typename RealScalar, typename Index>
static void tail_transpose_pack_real(RealScalar* EIGEN_RESTRICT dst_panel, const RealScalar* EIGEN_RESTRICT src,
Index src_stride, Index depth, Index tail) {
using Packet = typename packet_traits<RealScalar>::type;
constexpr int PacketSize = int(packet_traits<RealScalar>::size);
const Index peeled_tail = (tail / Index(PacketSize)) * Index(PacketSize);
const Index peeled_depth = numext::round_down(depth, Index(PacketSize));
Index i = 0;
for (; i < peeled_tail; i += Index(PacketSize)) {
Index k = 0;
for (; k < peeled_depth; k += Index(PacketSize)) {
PacketBlock<Packet, PacketSize> block;
for (int p = 0; p < PacketSize; ++p) {
block.packet[p] = ploadu<Packet>(src + (i + Index(p)) * src_stride + k);
}
ptranspose(block);
for (int p = 0; p < PacketSize; ++p) {
Packet row = block.packet[p];
EIGEN_IF_CONSTEXPR (NegateOddRows) {
if (((k + Index(p)) & Index(1)) != Index(0)) row = pnegate(row);
}
pstoreu(dst_panel + (k + Index(p)) * tail + i, row);
}
}
for (; k < depth; ++k) {
const bool negate = NegateOddRows && ((k & Index(1)) != Index(0));
for (Index p = 0; p < Index(PacketSize); ++p) {
const RealScalar v = src[(i + p) * src_stride + k];
dst_panel[k * tail + i + p] = negate ? -v : v;
}
}
}
for (; i < tail; ++i) {
for (Index k = 0; k < depth; ++k) {
const bool negate = NegateOddRows && ((k & Index(1)) != Index(0));
const RealScalar v = src[i * src_stride + k];
dst_panel[k * tail + i] = negate ? -v : v;
}
}
}
template <bool Conjugate, typename Scalar, typename Index>
static void tail_transpose_pack(Scalar* EIGEN_RESTRICT dst_panel, const Scalar* EIGEN_RESTRICT src, Index src_stride,
Index depth, Index tail) {
tail_transpose_pack_real<false>(dst_panel, src, src_stride, depth, tail);
}
template <bool Conjugate, typename RealScalar, typename Index>
static void tail_transpose_pack(std::complex<RealScalar>* EIGEN_RESTRICT dst_panel,
const std::complex<RealScalar>* EIGEN_RESTRICT src, Index src_stride, Index depth,
Index tail) {
tail_transpose_pack_real<Conjugate>(reinterpret_cast<RealScalar*>(dst_panel),
reinterpret_cast<const RealScalar*>(src), Index(2) * src_stride, Index(2) * depth,
tail);
}
// Tail panel of a deep block through the ZA transposer (sme_transpose_pack_partial); shallow ones and those of at
// most 4 columns keep the NEON tail_transpose_pack, which is faster there.
template <bool Conjugate, typename Scalar, typename Index>
__arm_locally_streaming __arm_new("za") static void sme_tail_pack_streaming(Scalar* dst_panel, const Scalar* src,
Index src_stride, Index depth, int tail) {
sme_transpose_pack<Conjugate>(dst_panel, src, src_stride, depth, tail);
}
template <bool Conjugate, typename Scalar, typename Index>
static EIGEN_ALWAYS_INLINE void tail_pack(Scalar* dst_panel, const Scalar* src, Index src_stride, Index depth,
Index tail) {
EIGEN_IF_CONSTEXPR (!NumTraits<Scalar>::IsComplex) {
if (tail > 4 && depth >= Index(4 * sme_block<Scalar>::nr)) {
sme_tail_pack_streaming<Conjugate>(dst_panel, src, src_stride, depth, static_cast<int>(tail));
return;
}
}
tail_transpose_pack<Conjugate>(dst_panel, src, src_stride, depth, tail);
}
// De-interleaving load and interleaving store of PS complex values.
static EIGEN_ALWAYS_INLINE void sme_neon_ld2(const float* p, Packet4f& re, Packet4f& im) {
const float32x4x2_t v = vld2q_f32(p);
re = v.val[0];
im = v.val[1];
}
static EIGEN_ALWAYS_INLINE void sme_neon_st2(float* p, Packet4f re, Packet4f im) {
float32x4x2_t v;
v.val[0] = re;
v.val[1] = im;
vst2q_f32(p, v);
}
static EIGEN_ALWAYS_INLINE void sme_neon_ld2(const double* p, Packet2d& re, Packet2d& im) {
const float64x2x2_t v = vld2q_f64(p);
re = v.val[0];
im = v.val[1];
}
static EIGEN_ALWAYS_INLINE void sme_neon_st2(double* p, Packet2d re, Packet2d im) {
float64x2x2_t v;
v.val[0] = re;
v.val[1] = im;
vst2q_f64(p, v);
}
// NEON copy packers for the direct-access, unit-stride case: dst[k*w + r] =
// src[r + k*src_stride], w contiguous scalars per depth step; complex panels
// split into the depth step's real then imaginary halves, conjugated if asked.
template <bool Conjugate, typename Scalar, typename Index>
static void neon_copy_panel(Scalar* EIGEN_RESTRICT dst, const Scalar* EIGEN_RESTRICT src, Index src_stride, Index depth,
Index w) {
using Packet = typename packet_traits<Scalar>::type;
constexpr Index PS = Index(packet_traits<Scalar>::size);
const Index peeled4 = numext::round_down(w, 4 * PS);
const Index peeled = numext::round_down(w, PS);
for (Index k = 0; k < depth; ++k) {
const Scalar* s = src + k * src_stride;
Scalar* d = dst + k * w;
Index r = 0;
for (; r < peeled4; r += 4 * PS) {
const Packet p0 = ploadu<Packet>(s + r), p1 = ploadu<Packet>(s + r + PS);
const Packet p2 = ploadu<Packet>(s + r + 2 * PS), p3 = ploadu<Packet>(s + r + 3 * PS);
pstoreu(d + r, p0);
pstoreu(d + r + PS, p1);
pstoreu(d + r + 2 * PS, p2);
pstoreu(d + r + 3 * PS, p3);
}
for (; r < peeled; r += PS) pstoreu(d + r, ploadu<Packet>(s + r));
for (; r < w; ++r) d[r] = s[r];
}
}
template <bool Conjugate, typename RealScalar, typename Index>
static void neon_copy_panel(std::complex<RealScalar>* EIGEN_RESTRICT dst,
const std::complex<RealScalar>* EIGEN_RESTRICT src, Index src_stride, Index depth,
Index w) {
using Packet = typename packet_traits<RealScalar>::type;
constexpr Index PS = Index(packet_traits<RealScalar>::size);
RealScalar* rd = reinterpret_cast<RealScalar*>(dst);
const RealScalar* rs = reinterpret_cast<const RealScalar*>(src);
const Index peeled = numext::round_down(w, PS);
for (Index k = 0; k < depth; ++k) {
const RealScalar* s = rs + k * 2 * src_stride;
RealScalar* d = rd + k * 2 * w;
Index r = 0;
for (; r < peeled; r += PS) {
Packet re, im;
sme_neon_ld2(s + 2 * r, re, im);
pstoreu(d + r, re);
pstoreu(d + w + r, Conjugate ? pnegate(im) : im);
}
for (; r < w; ++r) {
d[r] = s[2 * r];
d[w + r] = Conjugate ? -s[2 * r + 1] : s[2 * r + 1];
}
}
}
// ---------------------------------------------------------------------------
// Generic (mapper-based) packing fallback.
//
// The streaming pack_lhs_*/pack_rhs_* helpers take &lhs(0,0) once and walk it by
// raw pointer + lhs.stride(). That breaks for two DataMapper families:
// - TensorContractionSubMapper::operator() returns by value, so &lhs(0,0) is
// address-of-rvalue (a compile error, not just wrong results);
// - blas_data_mapper with Incr != 1 (inner-strided Maps, e.g. from
// TriangularSolverMatrix) can't be walked by stride() alone.
// These fall back to the mapper's packet/element interface, emitting the
// identical depth-major panel layout so gebp_kernel can't tell the paths apart.
// ---------------------------------------------------------------------------
// True iff DataMapper exposes .incr() (the blas_data_mapper family); others are
// unit-inner-stride by construction.
template <typename DataMapper, typename EnableIf = void>
struct sme_has_incr : std::false_type {};
template <typename DataMapper>
struct sme_has_incr<DataMapper, void_t<decltype(std::declval<const DataMapper&>().incr())>> : std::true_type {};
template <typename Index, typename DataMapper, std::enable_if_t<sme_has_incr<DataMapper>::value, bool> = true>
EIGEN_ALWAYS_INLINE Index sme_mapper_incr(const DataMapper& m) {
return static_cast<Index>(m.incr());
}
template <typename Index, typename DataMapper, std::enable_if_t<!sme_has_incr<DataMapper>::value, bool> = true>
EIGEN_ALWAYS_INLINE Index sme_mapper_incr(const DataMapper&) {
return Index(1);
}
// Whether operator()(i,j) returns an lvalue reference into caller storage (so
// &m(0,0) + stride walking is valid). False for by-value mappers (Tensor's).
template <typename DataMapper, typename Index>
struct sme_mapper_has_direct_access {
static constexpr bool value = std::is_lvalue_reference<decltype(std::declval<const DataMapper&>()(
std::declval<Index>(), std::declval<Index>()))>::value;
};
// Store one element of a packed depth step of width w. Real scalars land at
// dst_step[r]; complex ones split into the step's real and imaginary halves,
// which is the same base pointer reinterpreted, since a complex depth step of
// width w spans 2*w reals.
template <bool Conjugate, typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_pack_store(Scalar* dst_step, Index w, Index r, const Scalar& v) {
EIGEN_UNUSED_VARIABLE(w);
dst_step[r] = conj_if<Conjugate>()(v);
}
template <bool Conjugate, typename RealScalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_pack_store(std::complex<RealScalar>* dst_step, Index w, Index r,
const std::complex<RealScalar>& v) {
const std::complex<RealScalar> cv = conj_if<Conjugate>()(v);
RealScalar* p = reinterpret_cast<RealScalar*>(dst_step);
p[r] = numext::real(cv);
p[w + r] = numext::imag(cv);
}
// LHS fallback: pack via the mapper's packet interface, shared by both
// gemm_pack_lhs specializations. Taken by mappers without direct lvalue access
// (TensorContractionSubMapper returns by value) or with a non-unit inner
// stride. Vectorised with NEON packets, exactly like the generic packers drive
// these same mappers. Tensor sub-mappers (the hot path -- tensor contractions
// pack through this on both sides) have contiguous packet loads, but their
// ordinary operator()/loadPacket functions cannot be called from a streaming
// context. Inner-strided ColMajor blas mappers instead require gathers;
// streaming-mode gathers need FEAT_SME_FA64 (absent on e.g. Apple M4), while
// NEON's pgather uses scalar source loads and a contiguous packet store. The
// packet path assumes the mapper's packets advance the first index; that holds
// for ColMajor tensor and blas mappers, but not for RowMajor mappers, whose
// packets run along the storage-inner second index. RowMajor dispatches pass
// vectorise = false and take the scalar element loop. Complex scalars always
// take it too: a complex packet store would emit the interleaved layout, not the
// split one the kernel reads.
template <typename Scalar, int MR, typename Index, typename DataMapper, bool Conjugate, bool PanelMode>
void sme_pack_lhs_fallback(Scalar* dst_base, const DataMapper& lhs, Index depth, Index rows, Index dst_stride,
Index dst_offset, bool vectorise) {
using Packet = typename packet_traits<Scalar>::type;
constexpr Index PacketSize = Index(packet_traits<Scalar>::size);
constexpr bool HasPacketPath = !NumTraits<Scalar>::IsComplex;
for (Index i = 0; i < rows; i += MR) {
const Index w = numext::mini(rows - i, Index(MR));
Scalar* dst_panel = PanelMode ? dst_base + i * dst_stride + dst_offset * w : dst_base + i * depth;
const Index peeled_w = (vectorise && HasPacketPath) ? numext::round_down(w, Index(PacketSize)) : Index(0);
for (Index k = 0; k < depth; ++k) {
Scalar* dst_step = dst_panel + k * w;
Index r = 0;
for (; r < peeled_w; r += PacketSize) {
pstoreu(dst_step + r, lhs.template loadPacket<Packet>(i + r, k));
}
for (; r < w; ++r) {
sme_pack_store<Conjugate>(dst_step, w, r, lhs(i + r, k));
}
}
}
}
// The PacketSize column sub-mappers one packed column group loads from.
template <typename DataMapper, typename Index, std::size_t... Is>
EIGEN_ALWAYS_INLINE std::array<typename DataMapper::LinearMapper, sizeof...(Is)> sme_column_mappers(
const DataMapper& rhs, Index col, std::index_sequence<Is...>) {
return {{rhs.getLinearMapper(0, col + Index(Is))...}};
}
// RHS fallback, mirroring sme_pack_lhs_fallback (including the vectorise
// contract: LinearMapper packets must advance the first (depth) index). The
// packed layout wants consecutive columns contiguous while the mapper's
// packets run along the depth k, so PacketSize columns are loaded as packets
// along k and transposed in-register (the same LinearMapper + ptranspose
// scheme as the generic gemm_pack_rhs).
template <typename Scalar, int NR, typename Index, typename DataMapper, bool Conjugate, bool PanelMode>
void sme_pack_rhs_fallback(Scalar* dst_base, const DataMapper& rhs, Index depth, Index cols, Index dst_stride,
Index dst_offset, bool vectorise) {
using Packet = typename packet_traits<Scalar>::type;
using LinearMapper = typename DataMapper::LinearMapper;
constexpr int PacketSize = int(packet_traits<Scalar>::size);
constexpr bool HasPacketPath = !NumTraits<Scalar>::IsComplex;
const Index peeled_depth = (depth / Index(PacketSize)) * Index(PacketSize);
for (Index j = 0; j < cols; j += NR) {
const Index w = numext::mini(cols - j, Index(NR));
Scalar* dst_panel = PanelMode ? dst_base + j * dst_stride + dst_offset * w : dst_base + j * depth;
const Index peeled_w = (vectorise && HasPacketPath) ? numext::round_down(w, Index(PacketSize)) : Index(0);
Index c = 0;
for (; c < peeled_w; c += Index(PacketSize)) {
// Loop-invariant in k, but not hoisted out of the k loop by the compiler
// for a mapper that returns its sub-mappers by value -- which is the hot
// path here: tensor contractions pack through TensorContractionSubMapper.
const std::array<LinearMapper, PacketSize> dm =
sme_column_mappers(rhs, j + c, std::make_index_sequence<PacketSize>{});
Index k = 0;
for (; k < peeled_depth; k += Index(PacketSize)) {
PacketBlock<Packet, PacketSize> block;
for (int p = 0; p < PacketSize; ++p) {
block.packet[p] = dm[p].template loadPacket<Packet>(k);
}
ptranspose(block);
for (int p = 0; p < PacketSize; ++p) {
pstoreu(dst_panel + (k + Index(p)) * w + c, block.packet[p]);
}
}
for (; k < depth; ++k) {
for (Index p = 0; p < Index(PacketSize); ++p) {
sme_pack_store<Conjugate>(dst_panel + k * w, w, c + p, rhs(k, j + c + p));
}
}
}
for (; c < w; ++c) {
for (Index k = 0; k < depth; ++k) {
sme_pack_store<Conjugate>(dst_panel + k * w, w, c, rhs(k, j + c));
}
}
}
}
// NEON dispatch invariant, constants fitted on Apple M4 (SVL 512). Packers: panel depth (the stride in
// panel mode) <= sme_neon_max_depth && width <= sme_neon_max_panel. Kernel: both sides pass the packer
// test with strideA / strideB as the panel depths && (max(rows, cols) <= w(depth) || min <= thin_dim).
// So a NEON kernel reads NEON-packed panels, and ZA over NEON-packed panels (~40 ns/KB) is bounded by
// sme_neon_max_panel; the exception is SYRK, which packs B at the full size but runs 32x32 diagonal blocks.
#ifndef EIGEN_SME_NEON_MAX_DEPTH
template <typename Scalar>
struct sme_neon_max_depth : std::integral_constant<int, 16> {};
template <>
struct sme_neon_max_depth<float> : std::integral_constant<int, 24> {};
template <>
struct sme_neon_max_depth<std::complex<double>> : std::integral_constant<int, 8> {};
#else
template <typename Scalar>
struct sme_neon_max_depth : std::integral_constant<int, EIGEN_SME_NEON_MAX_DEPTH> {};
#endif
#ifndef EIGEN_SME_NEON_MAX_WIDTH
template <typename Scalar>
struct sme_neon_max_width : std::integral_constant<int, 48> {};
template <>
struct sme_neon_max_width<double> : std::integral_constant<int, 40> {};
template <>
struct sme_neon_max_width<std::complex<float>> : std::integral_constant<int, 16> {};
template <>
struct sme_neon_max_width<std::complex<double>> : std::integral_constant<int, 24> {};
#else
template <typename Scalar>
struct sme_neon_max_width : std::integral_constant<int, EIGEN_SME_NEON_MAX_WIDTH> {};
#endif
// Up to sme_neon_shallow_depth the NEON range widens to sme_neon_shallow_width:
// the blocked decompositions update sub-blocks of that depth in place, at
// offsets the ZA slice stores handle poorly.
#ifndef EIGEN_SME_NEON_SHALLOW_DEPTH
template <typename Scalar>
struct sme_neon_shallow_depth : std::integral_constant<int, 8> {};
#else
template <typename Scalar>
struct sme_neon_shallow_depth : std::integral_constant<int, EIGEN_SME_NEON_SHALLOW_DEPTH> {};
#endif
#ifndef EIGEN_SME_NEON_SHALLOW_WIDTH
template <typename Scalar>
struct sme_neon_shallow_width : std::integral_constant<int, 96> {};
template <>
struct sme_neon_shallow_width<double> : std::integral_constant<int, 64> {};
template <>
struct sme_neon_shallow_width<std::complex<float>> : std::integral_constant<int, 32> {};
template <>
struct sme_neon_shallow_width<std::complex<double>> : std::integral_constant<int, 24> {};
#else
template <typename Scalar>
struct sme_neon_shallow_width : std::integral_constant<int, EIGEN_SME_NEON_SHALLOW_WIDTH> {};
#endif
// No panel wider than this is packed with NEON at any depth: the triangular solvers slice a tall
// panel into depth-8 pieces for ZA. The packers cannot mirror the kernel's width rule, or a thin
// block's wide side would be streaming-packed and read by NEON, the costly direction; the price is
// a shallow panel in (w, this] read by ZA, 1.4x the streaming-packed time at 128x128x16 float.
#ifndef EIGEN_SME_NEON_MAX_PANEL
template <typename Scalar>
struct sme_neon_max_panel : std::integral_constant<int, 256> {};
template <typename RealScalar>
struct sme_neon_max_panel<std::complex<RealScalar>> : std::integral_constant<int, 128> {};
#else
template <typename Scalar>
struct sme_neon_max_panel : std::integral_constant<int, EIGEN_SME_NEON_MAX_PANEL> {};
#endif
// A block this narrow on one side runs on NEON whatever its other side: the
// blocked decompositions update long strips of that width in place.
#ifndef EIGEN_SME_NEON_THIN_DIM
template <typename Scalar>
struct sme_neon_thin_dim : std::integral_constant<int, 32> {};
template <>
struct sme_neon_thin_dim<std::complex<float>> : std::integral_constant<int, 8> {};
template <>
struct sme_neon_thin_dim<std::complex<double>> : std::integral_constant<int, 4> {};
#else
template <typename Scalar>
struct sme_neon_thin_dim : std::integral_constant<int, EIGEN_SME_NEON_THIN_DIM> {};
#endif
// `width` is the panel's rows (LHS) or cols (RHS).
template <typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE bool sme_pack_with_neon(Index depth, Index width) {
#if defined(EIGEN_SME_NO_NEON_SMALL_BLOCKS)
EIGEN_UNUSED_VARIABLE(depth);
EIGEN_UNUSED_VARIABLE(width);
return false;
#elif defined(EIGEN_SME_FORCE_NEON_SMALL_BLOCKS)
EIGEN_UNUSED_VARIABLE(depth);
EIGEN_UNUSED_VARIABLE(width);
return true;
#else
return depth <= Index(sme_neon_max_depth<Scalar>::value) && width <= Index(sme_neon_max_panel<Scalar>::value);
#endif
}
// strideA / strideB are the packed panels' depths, which exceed `depth` when
// the caller packed them in panel mode and runs the kernel on a slice.
template <typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE bool sme_kernel_with_neon(Index rows, Index cols, Index depth, Index strideA, Index strideB) {
#if defined(EIGEN_SME_NO_NEON_SMALL_BLOCKS) || defined(EIGEN_SME_FORCE_NEON_SMALL_BLOCKS)
EIGEN_UNUSED_VARIABLE(depth);
return sme_pack_with_neon<Scalar>(strideA, rows) && sme_pack_with_neon<Scalar>(strideB, cols);
#else
const Index wide = numext::maxi(rows, cols);
const Index w = depth <= Index(sme_neon_shallow_depth<Scalar>::value) ? Index(sme_neon_shallow_width<Scalar>::value)
: Index(sme_neon_max_width<Scalar>::value);
return sme_pack_with_neon<Scalar>(strideA, rows) && sme_pack_with_neon<Scalar>(strideB, cols) &&
(wide <= w || numext::mini(rows, cols) <= Index(sme_neon_thin_dim<Scalar>::value));
#endif
}
// Shared dispatch for the four gemm_pack specializations: raw-pointer walk
// when the mapper grants direct unit-inner-stride access, otherwise the
// packet/element fallback. Tag-dispatched so &m(0,0) is only compiled for
// lvalue mappers. UsePacketPath records whether the mapper's packets advance
// the index the fallback needs, independently of its direct-access category.
// In panel mode the call packs one depth slice of a panel `stride` deep that the
// kernel consumes whole (the triangular solvers and TRMM), so the mode decision
// uses the panel's depth, not the slice's.
template <bool UsePacketPath, bool PanelMode, typename Scalar, typename Index, typename DataMapper, typename DirectFn,
typename NeonFn, typename FallbackFn>
EIGEN_ALWAYS_INLINE void sme_dispatch_pack(DirectFn direct, NeonFn neon, FallbackFn fallback, Scalar* block,
const DataMapper& m, Index depth, Index n, Index stride, Index offset,
std::true_type /* direct access */) {
if (sme_mapper_incr<Index>(m) == 1) {
const Scalar* src = (n > 0 && depth > 0) ? &m(0, 0) : nullptr;
if (sme_pack_with_neon<Scalar>(PanelMode ? stride : depth, n)) {
neon(block, src, m.stride(), depth, n, stride, offset);
} else {
sme_fpsr_guard fpsr;
direct(block, src, m.stride(), depth, n, stride, offset);
}
} else {
fallback(block, m, depth, n, stride, offset, UsePacketPath);
}
}
template <bool UsePacketPath, bool PanelMode, typename Scalar, typename Index, typename DataMapper, typename DirectFn,
typename NeonFn, typename FallbackFn>
EIGEN_ALWAYS_INLINE void sme_dispatch_pack(DirectFn, NeonFn, FallbackFn fallback, Scalar* block, const DataMapper& m,
Index depth, Index n, Index stride, Index offset,
std::false_type /* no direct access */) {
fallback(block, m, depth, n, stride, offset, UsePacketPath);
}
/*****************************************************************************
* gebp_traits specializations for SME (float x float, double x double)
*
* Override mr and nr so that:
* - gemm_pack_lhs receives Pack1 = mr, creating uniform LHS panels
* - gemm_pack_rhs receives nr, creating uniform RHS panels
* - mc is rounded to a multiple of mr, nc to a multiple of nr
* - Cache blocking (kc, mc, nc) is recomputed accordingly
*
* We provide custom gemm_pack_lhs/gemm_pack_rhs specializations for both
* scalars, so both ColMajor and RowMajor source matrices produce an identical,
* simple packed format that the SME kernel consumes.
*
* Mixed-scalar products (e.g. MatrixXf * MatrixXcf) also instantiate
* gemm_pack_lhs<float, ...>, but with Pack1/nr from the generic
* gebp_traits<float, complex<float>> (mr=6, nr=4) and are consumed by the
* generic gebp_kernel, not the SME one. So the specializations below pin
* Pack1/nr_ to the SME block sizes: only the instantiation that feeds the SME
* gebp_kernel matches; mixed-scalar ones fall through to the generic template.
* This is load-bearing: it relies on no other consumer of the same scalar
* instantiating the packer with mr == the SME block size (holds today --
* generic float traits give mr <= 12). The kernel side is self-checking (the
* SME gebp_kernel static_asserts mr/nr against the block sizes, so a traits
* change breaks the build instead of silently mispairing packer and kernel);
* the packer side is enforced by the static_asserts below for the in-tree
* mixed-scalar traits (downstream code instantiating the packers with
* hand-picked mr/nr remains uncovered).
*****************************************************************************/
template <>
class gebp_traits<float, float, false, false, Architecture::Target, GEBPPacketFull>
: public gebp_traits<float, float, false, false, Architecture::Target, GEBPPacketHalf> {
public:
// The base class provides all the standard typedefs (LhsPacket, etc.)
// We only override the register-block sizes.
enum {
mr = kSmeMr, // LHS panel width
nr = kSmeNr // RHS panel width
};
};
// The packers do not know the opposite scalar type, so the SME block sizes are
// effectively SME-format tags. Ensure the in-tree mixed-scalar traits cannot
// select an SME packer whose output would be consumed by the generic kernel.
static_assert(int(gebp_traits<float, std::complex<float>>::mr) != kSmeMr,
"gebp_traits<float, complex<float>>::mr collides with kSmeMr: the SME gemm_pack_lhs would silently "
"emit SME panel layout for the generic gebp_kernel");
static_assert(int(gebp_traits<std::complex<float>, float>::nr) != kSmeNr,
"gebp_traits<complex<float>, float>::nr collides with kSmeNr: the SME gemm_pack_rhs would silently "
"emit SME panel layout for the generic gebp_kernel");
#ifdef EIGEN_VECTORIZE_SME_F64F64
template <>
class gebp_traits<double, double, false, false, Architecture::Target, GEBPPacketFull>
: public gebp_traits<double, double, false, false, Architecture::Target, GEBPPacketHalf> {
public:
// As above, only the register-block sizes are overridden.
static constexpr int mr = kSmeMrD;
static constexpr int nr = kSmeNrD;
};
static_assert(int(gebp_traits<double, std::complex<double>>::mr) != kSmeMrD,
"gebp_traits<double, complex<double>>::mr collides with kSmeMrD: the SME gemm_pack_lhs would silently "
"emit SME panel layout for the generic gebp_kernel");
static_assert(int(gebp_traits<std::complex<double>, double>::nr) != kSmeNrD,
"gebp_traits<complex<double>, double>::nr collides with kSmeNrD: the SME gemm_pack_rhs would silently "
"emit SME panel layout for the generic gebp_kernel");
#endif
// Complex block sizes, as above, but left open over the conjugation flags. A
// complex operand really does reach the kernel conjugated -- from an adjoint or
// conjugate product -- and the generic complex traits keep mr/nr independent of
// that, so pinning <false, false> here would hand a conjugated instantiation the
// generic block sizes while its kernel expects the SME ones.
template <bool ConjLhs_, bool ConjRhs_>
class gebp_traits<std::complex<float>, std::complex<float>, ConjLhs_, ConjRhs_, Architecture::Target, GEBPPacketFull>
: public gebp_traits<std::complex<float>, std::complex<float>, ConjLhs_, ConjRhs_, Architecture::Target,
GEBPPacketHalf> {
public:
static constexpr int mr = kSmeMrC;
static constexpr int nr = kSmeNrC;
};
// The mixed-scalar guard, with the roles of the two operands swapped relative
// to the real case: gemm_pack_lhs is instantiated with the LHS scalar and
// Traits::mr, gemm_pack_rhs with the RHS scalar and Traits::nr.
static_assert(int(gebp_traits<std::complex<float>, float>::mr) != kSmeMrC,
"gebp_traits<complex<float>, float>::mr collides with kSmeMrC: the SME gemm_pack_lhs would silently "
"emit SME panel layout for the generic gebp_kernel");
static_assert(int(gebp_traits<float, std::complex<float>>::nr) != kSmeNrC,
"gebp_traits<float, complex<float>>::nr collides with kSmeNrC: the SME gemm_pack_rhs would silently "
"emit SME panel layout for the generic gebp_kernel");
#ifdef EIGEN_VECTORIZE_SME_F64F64
template <bool ConjLhs_, bool ConjRhs_>
class gebp_traits<std::complex<double>, std::complex<double>, ConjLhs_, ConjRhs_, Architecture::Target, GEBPPacketFull>
: public gebp_traits<std::complex<double>, std::complex<double>, ConjLhs_, ConjRhs_, Architecture::Target,
GEBPPacketHalf> {
public:
static constexpr int mr = kSmeMrCD;
static constexpr int nr = kSmeNrCD;
};
static_assert(int(gebp_traits<std::complex<double>, double>::mr) != kSmeMrCD,
"gebp_traits<complex<double>, double>::mr collides with kSmeMrCD: the SME gemm_pack_lhs would silently "
"emit SME panel layout for the generic gebp_kernel");
static_assert(int(gebp_traits<double, std::complex<double>>::nr) != kSmeNrCD,
"gebp_traits<double, complex<double>>::nr collides with kSmeNrCD: the SME gemm_pack_rhs would silently "
"emit SME panel layout for the generic gebp_kernel");
#endif
/*****************************************************************************
* gemm_pack_lhs for SME (ColMajor source)
*
* Packs the LHS matrix into uniform panels of width mr.
* Each depth step k writes exactly MR contiguous scalars.
*****************************************************************************/
template <typename Scalar, int MR, typename Index, typename DataMapper, bool Conjugate, bool PanelMode>
struct sme_pack_lhs_colmajor {
// Non-streaming NEON copy of every panel, for the shallow blocks the NEON
// kernel consumes.
static EIGEN_ALWAYS_INLINE void pack_neon(Scalar* dst_base, const Scalar* EIGEN_RESTRICT src, Index src_stride,
Index depth, Index rows, Index dst_stride, Index dst_offset) {
for (Index i = 0; i < rows; i += MR) {
const Index w = numext::mini(Index(MR), rows - i);
Scalar* dst_panel = PanelMode ? dst_base + i * dst_stride + dst_offset * w : dst_base + i * depth;
neon_copy_panel<Conjugate>(dst_panel, src + i, src_stride, depth, w);
}
}
// EIGEN_DONT_INLINE: GCC (14.1 through trunk) may inline a __arm_locally_streaming function into a
// non-streaming caller and drop its mode switch. The other streaming entry points are __arm_new("za"),
// which GCC does not inline.
__arm_locally_streaming EIGEN_DONT_INLINE static void pack_direct(Scalar* dst_base, const Scalar* EIGEN_RESTRICT src,
Index src_stride, Index depth, Index rows,
Index dst_stride, Index dst_offset) {
const Index peeled_rows = (rows / MR) * MR;
const Index i0 = sme_pack_quad_panels<PanelMode>(dst_base, src, src_stride, depth, peeled_rows, Index(MR),
dst_stride, dst_offset);
// Full panels of width MR, streamed in svl-wide predicated chunks.
for (Index i = i0; i < peeled_rows; i += MR) {
Scalar* dst_panel = PanelMode ? dst_base + i * dst_stride + dst_offset * MR : dst_base + i * depth;
sve_copy_panel<Conjugate>(dst_panel, src + i, src_stride, depth, MR);
}
// Tail panel: rows < MR, use predicated SVE.
if (peeled_rows < rows) {
const Index tail = rows - peeled_rows;
Scalar* dst_panel =
PanelMode ? dst_base + peeled_rows * dst_stride + dst_offset * tail : dst_base + peeled_rows * depth;
sve_copy_panel<Conjugate>(dst_panel, src + peeled_rows, src_stride, depth, static_cast<int>(tail));
}
}
EIGEN_DONT_INLINE void operator()(Scalar* blockA, const DataMapper& lhs, Index depth, Index rows, Index stride = 0,
Index offset = 0) {
if (PanelMode) {
eigen_assert(stride >= depth && offset <= stride);
}
// Inner-strided ColMajor blas mappers' packets advance the row index, so
// the fallback may use them.
sme_dispatch_pack<true, PanelMode>(
&pack_direct, &pack_neon, &sme_pack_lhs_fallback<Scalar, MR, Index, DataMapper, Conjugate, PanelMode>, blockA,
lhs, depth, rows, stride, offset, bool_constant<sme_mapper_has_direct_access<DataMapper, Index>::value>{});
}
};
// RowMajor LHS packer -- SME in-ZA transpose.
//
// The packed output wants depth-major layout (MR rows contiguous per depth
// step) but the RowMajor source has rows contiguous (strided by depth per
// row). A natural SVE gather would be slow; instead we use ZA's 2D store
// as a free transpose: load svl rows as horizontal slices of a ZA tile,
// then read vertical slices to produce depth-major output (see
// sme_transpose_pack).
template <typename Scalar, int MR, typename Index, typename DataMapper, bool Conjugate, bool PanelMode>
struct sme_pack_lhs_rowmajor {
// Non-streaming NEON copy of every panel, for the shallow blocks the NEON
// kernel consumes.
static EIGEN_ALWAYS_INLINE void pack_neon(Scalar* dst_base, const Scalar* EIGEN_RESTRICT src, Index src_stride,
Index depth, Index rows, Index dst_stride, Index dst_offset) {
for (Index i = 0; i < rows; i += MR) {
const Index w = numext::mini(Index(MR), rows - i);
Scalar* dst_panel = PanelMode ? dst_base + i * dst_stride + dst_offset * w : dst_base + i * depth;
tail_transpose_pack<Conjugate>(dst_panel, src + i * src_stride, src_stride, depth, w);
}
}
__arm_locally_streaming __arm_new("za") static void pack_full_panels(Scalar* dst_base,
const Scalar* EIGEN_RESTRICT src,
Index src_stride, Index depth, Index peeled_rows,
Index dst_stride, Index dst_offset) {
for (Index i = 0; i < peeled_rows; i += MR) {
Scalar* dst_panel = PanelMode ? dst_base + i * dst_stride + dst_offset * MR : dst_base + i * depth;
sme_transpose_pack<Conjugate>(dst_panel, src + i * src_stride, src_stride, depth, MR);
}
}
static void pack_direct(Scalar* dst_base, const Scalar* EIGEN_RESTRICT src, Index src_stride, Index depth, Index rows,
Index dst_stride, Index dst_offset) {
const Index peeled_rows = numext::round_down(rows, MR);
if (peeled_rows > 0) {
pack_full_panels(dst_base, src, src_stride, depth, peeled_rows, dst_stride, dst_offset);
}
// Row tail (rows - peeled_rows in [1, MR-1]), at most once per call: see tail_pack.
if (peeled_rows < rows) {
const Index tail = rows - peeled_rows;
Scalar* dst_panel =
PanelMode ? dst_base + peeled_rows * dst_stride + dst_offset * tail : dst_base + peeled_rows * depth;
tail_pack<Conjugate>(dst_panel, src + peeled_rows * src_stride, src_stride, depth, tail);
}
}
EIGEN_DONT_INLINE void operator()(Scalar* blockA, const DataMapper& lhs, Index depth, Index rows, Index stride = 0,
Index offset = 0) {
if (PanelMode) {
eigen_assert(stride >= depth && offset <= stride);
}
// Inner-strided RowMajor blas mappers' packets advance the depth index, not
// the row index, so the fallback must stay scalar (see
// sme_pack_lhs_fallback).
sme_dispatch_pack<false, PanelMode>(
&pack_direct, &pack_neon, &sme_pack_lhs_fallback<Scalar, MR, Index, DataMapper, Conjugate, PanelMode>, blockA,
lhs, depth, rows, stride, offset, bool_constant<sme_mapper_has_direct_access<DataMapper, Index>::value>{});
}
};
/*****************************************************************************
* gemm_pack_rhs for SME (ColMajor source) -- SME in-ZA transpose, mirroring
* the RowMajor LHS packer.
*
* Packs the RHS matrix into panels of width nr. ColMajor source has
* columns contiguous; we load NR columns as horizontal ZA slices and then
* read verticals to produce depth-major packed output.
*****************************************************************************/
template <typename Scalar, int NR, typename Index, typename DataMapper, bool Conjugate, bool PanelMode>
struct sme_pack_rhs_colmajor {
// Non-streaming NEON copy of every panel, for the shallow blocks the NEON
// kernel consumes.
static EIGEN_ALWAYS_INLINE void pack_neon(Scalar* dst_base, const Scalar* EIGEN_RESTRICT src, Index src_stride,
Index depth, Index cols, Index dst_stride, Index dst_offset) {
for (Index i = 0; i < cols; i += NR) {
const Index w = numext::mini(Index(NR), cols - i);
Scalar* dst_panel = PanelMode ? dst_base + i * dst_stride + dst_offset * w : dst_base + i * depth;
tail_transpose_pack<Conjugate>(dst_panel, src + i * src_stride, src_stride, depth, w);
}
}
__arm_locally_streaming __arm_new("za") static void pack_full_panels(Scalar* dst_base,
const Scalar* EIGEN_RESTRICT src,
Index src_stride, Index depth, Index peeled_cols,
Index dst_stride, Index dst_offset) {
for (Index j = 0; j < peeled_cols; j += NR) {
Scalar* dst_panel = PanelMode ? dst_base + j * dst_stride + dst_offset * NR : dst_base + j * depth;
sme_transpose_pack<Conjugate>(dst_panel, src + j * src_stride, src_stride, depth, NR);
}
}
static void pack_direct(Scalar* dst_base, const Scalar* EIGEN_RESTRICT src, Index src_stride, Index depth, Index cols,
Index dst_stride, Index dst_offset) {
const Index peeled_cols = numext::round_down(cols, NR);
if (peeled_cols > 0) {
pack_full_panels(dst_base, src, src_stride, depth, peeled_cols, dst_stride, dst_offset);
}
// Col tail (cols - peeled_cols in [1, NR-1]), at most once per call: see tail_pack.
if (peeled_cols < cols) {
const Index tail = cols - peeled_cols;
Scalar* dst_panel =
PanelMode ? dst_base + peeled_cols * dst_stride + dst_offset * tail : dst_base + peeled_cols * depth;
tail_pack<Conjugate>(dst_panel, src + peeled_cols * src_stride, src_stride, depth, tail);
}
}
EIGEN_DONT_INLINE void operator()(Scalar* blockB, const DataMapper& rhs, Index depth, Index cols, Index stride = 0,
Index offset = 0) {
if (PanelMode) {
eigen_assert(stride >= depth && offset <= stride);
}
// Inner-strided ColMajor blas mappers' LinearMapper packets advance the
// depth index, which is what the fallback transposes.
sme_dispatch_pack<true, PanelMode>(
&pack_direct, &pack_neon, &sme_pack_rhs_fallback<Scalar, NR, Index, DataMapper, Conjugate, PanelMode>, blockB,
rhs, depth, cols, stride, offset, bool_constant<sme_mapper_has_direct_access<DataMapper, Index>::value>{});
}
};
// RowMajor RHS packer -- streaming SVE copy (mirrors the ColMajor LHS packer).
// Rows are contiguous in the source, so each depth-step is NR contiguous scalars.
template <typename Scalar, int NR, typename Index, typename DataMapper, bool Conjugate, bool PanelMode>
struct sme_pack_rhs_rowmajor {
// Non-streaming NEON copy of every panel, for the shallow blocks the NEON
// kernel consumes.
static EIGEN_ALWAYS_INLINE void pack_neon(Scalar* dst_base, const Scalar* EIGEN_RESTRICT src, Index src_stride,
Index depth, Index cols, Index dst_stride, Index dst_offset) {
for (Index i = 0; i < cols; i += NR) {
const Index w = numext::mini(Index(NR), cols - i);
Scalar* dst_panel = PanelMode ? dst_base + i * dst_stride + dst_offset * w : dst_base + i * depth;
neon_copy_panel<Conjugate>(dst_panel, src + i, src_stride, depth, w);
}
}
// EIGEN_DONT_INLINE: as in sme_pack_lhs_colmajor::pack_direct.
__arm_locally_streaming EIGEN_DONT_INLINE static void pack_direct(Scalar* dst_base, const Scalar* EIGEN_RESTRICT src,
Index src_stride, Index depth, Index cols,
Index dst_stride, Index dst_offset) {
const Index peeled_cols = (cols / NR) * NR;
for (Index j = 0; j < peeled_cols; j += NR) {
Scalar* dst_panel = PanelMode ? dst_base + j * dst_stride + dst_offset * NR : dst_base + j * depth;
sve_copy_panel<Conjugate>(dst_panel, src + j, src_stride, depth, NR);
}
if (peeled_cols < cols) {
const Index tail = cols - peeled_cols;
Scalar* dst_panel =
PanelMode ? dst_base + peeled_cols * dst_stride + dst_offset * tail : dst_base + peeled_cols * depth;
sve_copy_panel<Conjugate>(dst_panel, src + peeled_cols, src_stride, depth, static_cast<int>(tail));
}
}
EIGEN_DONT_INLINE void operator()(Scalar* blockB, const DataMapper& rhs, Index depth, Index cols, Index stride = 0,
Index offset = 0) {
if (PanelMode) {
eigen_assert(stride >= depth && offset <= stride);
}
// Inner-strided RowMajor blas mappers' LinearMapper packets advance the
// column index, not depth, so the fallback must stay scalar (see
// sme_pack_rhs_fallback).
sme_dispatch_pack<false, PanelMode>(
&pack_direct, &pack_neon, &sme_pack_rhs_fallback<Scalar, NR, Index, DataMapper, Conjugate, PanelMode>, blockB,
rhs, depth, cols, stride, offset, bool_constant<sme_mapper_has_direct_access<DataMapper, Index>::value>{});
}
};
// Pack1/nr_ are pinned to the SME block sizes (rather than left open) so these
// specializations only match consumers that actually feed the SME gebp_kernel
// -- see "Mixed-scalar products" in the gebp_traits doc comment above.
#define EIGEN_SME_DECLARE_GEMM_PACKERS(SCALAR, MR, NR) \
template <typename Index, typename DataMapper, int Pack2, typename Packet, bool Conjugate, bool PanelMode> \
struct gemm_pack_lhs<SCALAR, Index, DataMapper, MR, Pack2, Packet, ColMajor, Conjugate, PanelMode> \
: sme_pack_lhs_colmajor<SCALAR, MR, Index, DataMapper, Conjugate, PanelMode> {}; \
\
template <typename Index, typename DataMapper, int Pack2, typename Packet, bool Conjugate, bool PanelMode> \
struct gemm_pack_lhs<SCALAR, Index, DataMapper, MR, Pack2, Packet, RowMajor, Conjugate, PanelMode> \
: sme_pack_lhs_rowmajor<SCALAR, MR, Index, DataMapper, Conjugate, PanelMode> {}; \
\
template <typename Index, typename DataMapper, bool Conjugate, bool PanelMode> \
struct gemm_pack_rhs<SCALAR, Index, DataMapper, NR, ColMajor, Conjugate, PanelMode> \
: sme_pack_rhs_colmajor<SCALAR, NR, Index, DataMapper, Conjugate, PanelMode> {}; \
\
template <typename Index, typename DataMapper, bool Conjugate, bool PanelMode> \
struct gemm_pack_rhs<SCALAR, Index, DataMapper, NR, RowMajor, Conjugate, PanelMode> \
: sme_pack_rhs_rowmajor<SCALAR, NR, Index, DataMapper, Conjugate, PanelMode> {};
EIGEN_SME_DECLARE_GEMM_PACKERS(float, kSmeMr, kSmeNr)
EIGEN_SME_DECLARE_GEMM_PACKERS(std::complex<float>, kSmeMrC, kSmeNrC)
#ifdef EIGEN_VECTORIZE_SME_F64F64
EIGEN_SME_DECLARE_GEMM_PACKERS(double, kSmeMrD, kSmeNrD)
EIGEN_SME_DECLARE_GEMM_PACKERS(std::complex<double>, kSmeMrCD, kSmeNrCD)
#endif
#undef EIGEN_SME_DECLARE_GEMM_PACKERS
/*****************************************************************************
* sme_store_za_tile -- Store one ZA tile back to C with alpha scaling.
*
* `pw` is the row-predicate width for this tile, `cw` the col-predicate width
* (both <= the runtime svl).
*****************************************************************************/
template <typename Scalar, int TileId, typename Index>
EIGEN_ALWAYS_INLINE void sme_store_za_tile(Scalar* EIGEN_RESTRICT C, Index C_stride_row, Index C_stride_col,
Scalar alpha, Index row_start, int pw, Index col_start,
int cw) __arm_streaming __arm_inout("za") {
using Traits = sme_packet_traits<Scalar>;
using Vec = typename Traits::type;
const svbool_t pg_m = Traits::whilelt(0, pw);
const svbool_t pg_n = Traits::whilelt(0, cw);
// FMLA and FADD have equal latency/throughput on ARMv9 cores, and
// multiplying by alpha=1.0 is exact in IEEE-754 so the FMLA form is
// bit-identical to FADD in that case. A single unconditional FMLA
// keeps the store compact and measures no worse (and a few percent
// better on small matrices, where the branch would otherwise disrupt
// instruction scheduling).
const Vec vzero = pset1<Vec>(Scalar(0));
const Vec valpha = pset1<Vec>(alpha);
// Two C slices are loaded before either is stored: a C line the caller wrote
// from non-streaming code just before the kernel does not forward across the
// mode switch on Apple M4, and a serial load/store pays that latency per slice.
// C = A*B meets the condition on every call, since evalTo zeroes the
// destination first. SVE vectors are sizeless, hence the spelled-out pair.
if (C_stride_row == 1) {
// Column-major C: extract vertical slices (columns of the ZA tile)
int ci = 0;
for (; ci + 2 <= cw; ci += 2) {
Scalar* p0 = C + row_start + (col_start + ci) * C_stride_col;
Scalar* p1 = p0 + C_stride_col;
Vec c0 = ploadu(pg_m, p0);
Vec c1 = ploadu(pg_m, p1);
pstoreu(pg_m, p0, pmadd(pg_m, sme_read_ver_za<TileId>(vzero, pg_m, (uint32_t)ci), valpha, c0));
pstoreu(pg_m, p1, pmadd(pg_m, sme_read_ver_za<TileId>(vzero, pg_m, (uint32_t)(ci + 1)), valpha, c1));
}
if (ci < cw) {
Scalar* pC = C + row_start + (col_start + ci) * C_stride_col;
Vec vc = ploadu(pg_m, pC);
pstoreu(pg_m, pC, pmadd(pg_m, sme_read_ver_za<TileId>(vzero, pg_m, (uint32_t)ci), valpha, vc));
}
} else if (C_stride_col == 1) {
// Row-major C: extract horizontal slices (rows of the ZA tile)
int ri = 0;
for (; ri + 2 <= pw; ri += 2) {
Scalar* p0 = C + (row_start + ri) * C_stride_row + col_start;
Scalar* p1 = p0 + C_stride_row;
Vec c0 = ploadu(pg_n, p0);
Vec c1 = ploadu(pg_n, p1);
pstoreu(pg_n, p0, pmadd(pg_n, sme_read_hor_za<TileId>(vzero, pg_n, (uint32_t)ri), valpha, c0));
pstoreu(pg_n, p1, pmadd(pg_n, sme_read_hor_za<TileId>(vzero, pg_n, (uint32_t)(ri + 1)), valpha, c1));
}
if (ri < pw) {
Scalar* pC = C + (row_start + ri) * C_stride_row + col_start;
Vec vc = ploadu(pg_n, pC);
pstoreu(pg_n, pC, pmadd(pg_n, sme_read_hor_za<TileId>(vzero, pg_n, (uint32_t)ri), valpha, vc));
}
} else {
// General stride: extract rows to temp buffer, scatter to C. scratch
// holds one ZA row; every caller passes cw <= min(svl, nr) (a tile
// never spans more than the logical block), so nr is a static
// bound independent of the runtime svl.
Scalar scratch[sme_block<Scalar>::nr];
for (int ri = 0; ri < pw; ++ri) {
Vec vres = sme_read_hor_za<TileId>(vzero, pg_n, (uint32_t)ri);
vres = pmul(pg_n, vres, valpha);
pstoreu(pg_n, scratch, vres);
for (int ci = 0; ci < cw; ++ci) {
C[(row_start + ri) * C_stride_row + (col_start + ci) * C_stride_col] += scratch[ci];
}
}
}
}
/*****************************************************************************
* sme_store_2x2_grid -- store the (up to) 2x2 grid of svl x svl ZA tiles.
*
* Tile layout: 0 = (row-lo, col-lo) 1 = (row-lo, col-hi)
* 2 = (row-hi, col-lo) 3 = (row-hi, col-hi)
* The col-hi tiles (1, 3) are stored only when chi > 0 and the row-hi tiles
* (2, 3) only when rhi > 0, so a single tile, a 1x2/2x1 pair, or the full grid
* all route through here. Runs once per sub-block pass, after a depth loop
* that dwarfs it, so the branches cost nothing and predict perfectly (the
* pattern repeats across blocks).
*****************************************************************************/
// Full 2*svl x 2*svl block into column-major C: four columns per vertical four-slice read of the two tiles that
// share them, each column loaded and stored whole with two-vector accesses, all four loaded before any is stored.
template <int TileLo, int TileHi, typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_store_tile_pair_colmajor(Scalar* EIGEN_RESTRICT C, Index ldc, Scalar alpha,
int svl) __arm_streaming __arm_inout("za") {
using Traits = sme_packet_traits<Scalar>;
const svcount_t pn = Traits::ptrue_c();
const svbool_t pg = Traits::ptrue();
const typename Traits::type valpha = pset1<typename Traits::type>(alpha);
for (int c = 0; c < svl; c += 4) {
const auto lo = sme_read_ver_za_vg4<TileLo, Scalar>(uint32_t(c));
const auto hi = sme_read_ver_za_vg4<TileHi, Scalar>(uint32_t(c));
Scalar* p = C + Index(c) * ldc;
const auto c0 = ploadu_x2(pn, p), c1 = ploadu_x2(pn, p + ldc), c2 = ploadu_x2(pn, p + 2 * ldc),
c3 = ploadu_x2(pn, p + 3 * ldc);
pstoreu_x2(pn, p,
pcreate(pmadd(pg, pget<0>(lo), valpha, pget<0>(c0)), pmadd(pg, pget<0>(hi), valpha, pget<1>(c0))));
pstoreu_x2(pn, p + ldc,
pcreate(pmadd(pg, pget<1>(lo), valpha, pget<0>(c1)), pmadd(pg, pget<1>(hi), valpha, pget<1>(c1))));
pstoreu_x2(pn, p + 2 * ldc,
pcreate(pmadd(pg, pget<2>(lo), valpha, pget<0>(c2)), pmadd(pg, pget<2>(hi), valpha, pget<1>(c2))));
pstoreu_x2(pn, p + 3 * ldc,
pcreate(pmadd(pg, pget<3>(lo), valpha, pget<0>(c3)), pmadd(pg, pget<3>(hi), valpha, pget<1>(c3))));
}
}
template <typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_store_2x2_grid(Scalar* EIGEN_RESTRICT C, Index C_stride_row, Index C_stride_col,
Scalar alpha, Index row_start, int rlo, int rhi, Index col_start, int clo,
int chi) __arm_streaming __arm_inout("za") {
const int svl = sme_packet_traits<Scalar>::size();
if (svl >= 4 && C_stride_row == 1 && rlo == svl && rhi == svl && clo == svl && chi == svl) {
Scalar* c0 = C + row_start + col_start * C_stride_col;
sme_store_tile_pair_colmajor<0, 2>(c0, C_stride_col, alpha, svl);
sme_store_tile_pair_colmajor<1, 3>(c0 + Index(svl) * C_stride_col, C_stride_col, alpha, svl);
return;
}
sme_store_za_tile<Scalar, 0>(C, C_stride_row, C_stride_col, alpha, row_start, rlo, col_start, clo);
if (chi > 0) {
sme_store_za_tile<Scalar, 1>(C, C_stride_row, C_stride_col, alpha, row_start, rlo, col_start + svl, chi);
}
if (rhi > 0) {
sme_store_za_tile<Scalar, 2>(C, C_stride_row, C_stride_col, alpha, row_start + svl, rhi, col_start, clo);
if (chi > 0) {
sme_store_za_tile<Scalar, 3>(C, C_stride_row, C_stride_col, alpha, row_start + svl, rhi, col_start + svl, chi);
}
}
}
// One depth step's worth of the exact-match grid: the four FMOPAs that take the
// lo/hi halves of a packed A column and a packed B column and accumulate the
// 2x2 ZA-tile outer product. `all` is the all-true predicate because this is
// only used on the exact-match path, where the block fills the grid, so
// factoring it out is identical to the inline form.
template <typename Scalar>
static EIGEN_ALWAYS_INLINE void outer_product_2x2(
typename sme_packet_traits<Scalar>::type a_lo, typename sme_packet_traits<Scalar>::type a_hi,
typename sme_packet_traits<Scalar>::type b_lo,
typename sme_packet_traits<Scalar>::type b_hi) __arm_streaming __arm_inout("za") {
const svbool_t all = sme_packet_traits<Scalar>::ptrue();
sme_mopa<0>(all, all, a_lo, b_lo);
sme_mopa<1>(all, all, a_lo, b_hi);
sme_mopa<2>(all, all, a_hi, b_lo);
sme_mopa<3>(all, all, a_hi, b_hi);
}
/*****************************************************************************
* Complex accumulator: a pair of ZA tiles holding the real and imaginary
* halves of one grid cell.
*
* With a = ar + i*sa*ai and b = br + i*sb*bi, where sa is -1 when the LHS is
* conjugated and sb likewise for the RHS,
*
* re(a*b) = ar*br - (sa*sb) * ai*bi, im(a*b) = sb * ar*bi + sa * ai*br,
*
* so all four real outer products differ only in whether they accumulate
* (FMOPA) or subtract (FMOPS) -- a compile-time choice, with no work in the
* depth loop and no separate conjugating packer.
*
* Slices come back out through sme_read_slice, which applies the complex
* alpha and interleaves the halves with ZIP1/ZIP2 into the two vectors that
* cover one slice's worth of contiguous complex results. The kernel that
* folds a real alpha keeps them deinterleaved instead (see
* sme_accumulate_pair_real_alpha).
*
* A ZA tile number is an instruction immediate, so cells outside the tile grid
* (complex<float> has two tile pairs, hence a single grid row) are dropped by
* the InGrid specialization rather than by a runtime guard, which would still
* have to name an in-range tile.
*****************************************************************************/
// One slice of a tile pair, re-interleaved into the two vectors that cover its
// complex results: `lo` the first half, `hi` the second. ScaleByAlpha applies
// the complex alpha, four predicated FP ops the caller skips when alpha is 1
// (see sme_store_za_pair) -- streaming-mode FP is de-rated enough on Apple M4
// that those four cost about as much as the rest of the slice.
template <typename RealScalar, int TileRe, int TileIm, bool Vertical, bool ScaleByAlpha>
EIGEN_ALWAYS_INLINE void sme_read_slice(
svbool_t pg, typename sme_packet_traits<RealScalar>::type valpha_re,
typename sme_packet_traits<RealScalar>::type valpha_im, typename sme_packet_traits<RealScalar>::type vzero,
uint32_t slice, typename sme_packet_traits<RealScalar>::type& lo,
typename sme_packet_traits<RealScalar>::type& hi) __arm_streaming __arm_inout("za") {
using Vec = typename sme_packet_traits<RealScalar>::type;
Vec re, im;
EIGEN_IF_CONSTEXPR (Vertical) {
re = sme_read_ver_za<TileRe>(vzero, pg, slice);
im = sme_read_ver_za<TileIm>(vzero, pg, slice);
} else {
re = sme_read_hor_za<TileRe>(vzero, pg, slice);
im = sme_read_hor_za<TileIm>(vzero, pg, slice);
}
EIGEN_IF_CONSTEXPR (ScaleByAlpha) {
const Vec out_re = pnmadd(pg, im, valpha_im, pmul(pg, re, valpha_re));
const Vec out_im = pmadd(pg, re, valpha_im, pmul(pg, im, valpha_re));
lo = pzip1(out_re, out_im);
hi = pzip2(out_re, out_im);
} else {
lo = pzip1(re, im);
hi = pzip2(re, im);
}
}
// Accumulate a tile pair's `slices` slices into C along its contiguous axis,
// `step` reals apart. `lanes` is twice a slice's complex count; when it fits
// one vector the high half's predicate is empty, so its load and store are
// no-ops even though the destination has nothing at p + svl to point at.
template <typename RealScalar, int TileRe, int TileIm, bool Vertical, bool ScaleByAlpha, typename Index>
EIGEN_ALWAYS_INLINE void sme_accumulate_pair_impl(
RealScalar* EIGEN_RESTRICT p, Index step, int slices, int lanes, svbool_t pg,
typename sme_packet_traits<RealScalar>::type valpha_re, typename sme_packet_traits<RealScalar>::type valpha_im,
typename sme_packet_traits<RealScalar>::type vzero) __arm_streaming __arm_inout("za") {
using Traits = sme_packet_traits<RealScalar>;
using Vec = typename Traits::type;
const int svl = Traits::size();
const svbool_t pl0 = Traits::whilelt(0, lanes);
const svbool_t pl1 = Traits::whilelt(svl, lanes);
for (int s = 0; s < slices; ++s, p += step) {
Vec lo, hi;
sme_read_slice<RealScalar, TileRe, TileIm, Vertical, ScaleByAlpha>(pg, valpha_re, valpha_im, vzero, uint32_t(s), lo,
hi);
pstoreu(pl0, p, padd(pl0, ploadu(pl0, p), lo));
// pl1 is all-false when one vector covers the slice, and an inactive lane
// neither reads nor writes -- so this needs no `lanes > svl` guard, only an
// address the destination is allowed to form.
RealScalar* EIGEN_RESTRICT phi = sme_offset(p, Index(svl));
pstoreu(pl1, phi, padd(pl1, ploadu(pl1, phi), hi));
}
}
// A real alpha on slices spanning two vectors, in the FoldRealAlpha kernel:
// LD2/ST2 keep C deinterleaved with one predicate lane per complex result, so
// each half is one FMA, c + alpha*acc, as in the real-scalar store. A single
// FMA rounds once, so it overflows only when the exact result does. This
// covers C.noalias() -= A*B, whose alpha is -1.
//
// `limit`, the slices left in the C block from the first one, bounds the
// prefetch to the block (unbounded, it measured 0.45-0.5x on 32^3 and 64^3).
// The prefetch runs 8 slices ahead of the C load that heads each FMA; without
// it the fold ran at 0.94x of that at 1024^3. The barrier keeps the prefetch
// address setup inside its branch.
//
// A complex alpha keeps sme_accumulate_pair_impl, which forms alpha*acc before
// adding C. Folding C into either of its two FMAs, e.g. (c_re + re*ar) - im*ai,
// saves two ops per slice but overflows when C and the first product exceed
// the range before the second cancels them (!3139 review: acc = (h,h),
// alpha = (1,1), C = (3h,0)). On Apple M4, LD2/ST2 measured no gain with
// unchanged arithmetic and 0.68-0.75x on one-vector slices, and FCMLA 0.70x
// (#3129).
template <typename RealScalar, int TileRe, int TileIm, bool Vertical, typename Index>
EIGEN_ALWAYS_INLINE void sme_accumulate_pair_real_alpha(
RealScalar* EIGEN_RESTRICT p, Index step, int slices, Index limit, svbool_t pg,
typename sme_packet_traits<RealScalar>::type valpha,
typename sme_packet_traits<RealScalar>::type vzero) __arm_streaming __arm_inout("za") {
using Traits = sme_packet_traits<RealScalar>;
using Vec = typename Traits::type;
const int svl = Traits::size();
constexpr int kPrefetchSlices = 8;
for (int s = 0; s < slices; ++s, p += step) {
if (s + kPrefetchSlices < limit) {
RealScalar* pf = p;
EIGEN_OPTIMIZATION_BARRIER(pf)
pf = sme_offset(pf, Index(kPrefetchSlices) * step);
__builtin_prefetch(pf, 1, 3);
__builtin_prefetch(sme_offset(pf, Index(svl)), 1, 3);
}
Vec re, im;
EIGEN_IF_CONSTEXPR (Vertical) {
re = sme_read_ver_za<TileRe>(vzero, pg, uint32_t(s));
im = sme_read_ver_za<TileIm>(vzero, pg, uint32_t(s));
} else {
re = sme_read_hor_za<TileRe>(vzero, pg, uint32_t(s));
im = sme_read_hor_za<TileIm>(vzero, pg, uint32_t(s));
}
const typename Traits::type_x2 c = pld2(pg, p);
pst2(pg, p, pcreate(pmadd(pg, re, valpha, pget<0>(c)), pmadd(pg, im, valpha, pget<1>(c))));
}
}
// FoldRealAlpha selects the kernel instantiation. The folding kernel only
// sees a real alpha other than 1 (see sme_gebp_dispatch); the other kernel
// keeps exactly the unscaled and complex-alpha paths, so the fold's code never
// reaches products that cannot use it.
template <typename RealScalar, int TileRe, int TileIm, bool Vertical, bool FoldRealAlpha, typename Index>
EIGEN_ALWAYS_INLINE void sme_accumulate_pair(
bool scale_by_alpha, RealScalar* EIGEN_RESTRICT p, Index step, int slices, Index limit, int lanes, svbool_t pg,
typename sme_packet_traits<RealScalar>::type valpha_re, typename sme_packet_traits<RealScalar>::type valpha_im,
typename sme_packet_traits<RealScalar>::type vzero) __arm_streaming __arm_inout("za") {
if (FoldRealAlpha && lanes > sme_packet_traits<RealScalar>::size()) {
sme_accumulate_pair_real_alpha<RealScalar, TileRe, TileIm, Vertical>(p, step, slices, limit, pg, valpha_re, vzero);
} else if (scale_by_alpha) {
sme_accumulate_pair_impl<RealScalar, TileRe, TileIm, Vertical, true>(p, step, slices, lanes, pg, valpha_re,
valpha_im, vzero);
} else {
sme_accumulate_pair_impl<RealScalar, TileRe, TileIm, Vertical, false>(p, step, slices, lanes, pg, valpha_re,
valpha_im, vzero);
}
}
// Store one complex tile pair back to C. `pw` is the row-predicate width for
// this cell and `cw` the column one, both <= the runtime svl.
template <typename RealScalar, int TileRe, int TileIm, bool FoldRealAlpha, typename Index>
EIGEN_ALWAYS_INLINE void sme_store_za_pair(std::complex<RealScalar>* EIGEN_RESTRICT C, Index C_stride_row,
Index C_stride_col, std::complex<RealScalar> alpha, Index row_start, int pw,
Index col_start, int cw, Index rows,
Index cols) __arm_streaming __arm_inout("za") {
using Scalar = std::complex<RealScalar>;
using Traits = sme_packet_traits<RealScalar>;
using Vec = typename Traits::type;
const int svl = Traits::size();
const svbool_t pg_m = Traits::whilelt(0, pw);
const svbool_t pg_n = Traits::whilelt(0, cw);
const Vec vzero = pset1<Vec>(RealScalar(0));
// std::complex's accessors are ordinary functions, which clang cannot inline
// into a streaming context; the resulting mode switch would sit in this loop.
// A complex is layout-compatible with its two-element real array, so read the
// parts through that view instead.
const RealScalar* alpha_parts = reinterpret_cast<const RealScalar*>(&alpha);
const Vec valpha_re = pset1<Vec>(alpha_parts[0]);
const Vec valpha_im = pset1<Vec>(alpha_parts[1]);
// Scaling by 1 + 0i is exact, so skipping it is bit-identical -- and it is by
// far the common case, since a plain product carries alpha = 1.
// With FoldRealAlpha, sme_gebp_dispatch has already established that alpha
// is real and not 1.
const bool scale = !(alpha_parts[0] == RealScalar(1) && alpha_parts[1] == RealScalar(0));
RealScalar* EIGEN_RESTRICT rC = reinterpret_cast<RealScalar*>(C);
if (C_stride_row == 1) {
// Column-major C: vertical slices are the tile pair's columns, and one
// slice is pw contiguous complex results, i.e. 2*pw reals.
RealScalar* p = rC + Index(2) * (row_start + col_start * C_stride_col);
sme_accumulate_pair<RealScalar, TileRe, TileIm, true, FoldRealAlpha>(
scale, p, Index(2) * C_stride_col, cw, cols - col_start, 2 * pw, pg_m, valpha_re, valpha_im, vzero);
} else if (C_stride_col == 1) {
// Row-major C: horizontal slices are the tile pair's rows.
RealScalar* p = rC + Index(2) * (row_start * C_stride_row + col_start);
sme_accumulate_pair<RealScalar, TileRe, TileIm, false, FoldRealAlpha>(
scale, p, Index(2) * C_stride_row, pw, rows - row_start, 2 * cw, pg_n, valpha_re, valpha_im, vzero);
} else {
// General stride: interleave a row into a temp buffer, scatter to C. Every
// caller passes cw <= min(svl, nr), so nr is a static bound on the buffer,
// independent of the runtime svl. This path is scalar anyway, so it always
// takes the scaling form.
Scalar scratch[sme_block<Scalar>::nr];
RealScalar* rscratch = reinterpret_cast<RealScalar*>(scratch);
const int lanes = 2 * cw;
const svbool_t pl0 = Traits::whilelt(0, lanes);
const svbool_t pl1 = Traits::whilelt(svl, lanes);
for (int ri = 0; ri < pw; ++ri) {
Vec lo, hi;
sme_read_slice<RealScalar, TileRe, TileIm, false, true>(pg_n, valpha_re, valpha_im, vzero, uint32_t(ri), lo, hi);
pstoreu(pl0, rscratch, lo);
// scratch is nr complex, i.e. 2*nr >= 2*svl reals, so rscratch + svl is
// always in bounds; pl1 is all-false when one vector already covers cw.
pstoreu(pl1, rscratch + svl, hi);
for (int ci = 0; ci < cw; ++ci) {
C[(row_start + ri) * C_stride_row + (col_start + ci) * C_stride_col] += scratch[ci];
}
}
}
}
// Grid cell (R, C) of a complex block: its tile pair, the four signed outer
// products that feed it, and its store.
template <typename Scalar, int R, int C, bool ConjLhs, bool ConjRhs,
bool InGrid = (R < sme_block<Scalar>::kGridRows && C < sme_block<Scalar>::kGridCols)>
struct sme_complex_cell {
using RealScalar = typename NumTraits<Scalar>::Real;
using Vec = typename sme_packet_traits<RealScalar>::type;
static constexpr int kTileRe = 2 * (R * sme_block<Scalar>::kGridCols + C);
static constexpr int kTileIm = kTileRe + 1;
static EIGEN_ALWAYS_INLINE void accumulate(svbool_t pm, svbool_t pn, Vec a_re, Vec a_im, Vec b_re,
Vec b_im) __arm_streaming __arm_inout("za") {
sme_mopa_signed<kTileRe, false>(pm, pn, a_re, b_re);
sme_mopa_signed<kTileRe, ConjLhs == ConjRhs>(pm, pn, a_im, b_im);
sme_mopa_signed<kTileIm, ConjRhs>(pm, pn, a_re, b_im);
sme_mopa_signed<kTileIm, ConjLhs>(pm, pn, a_im, b_re);
}
template <bool FoldRealAlpha, typename Index>
static EIGEN_ALWAYS_INLINE void store(Scalar* EIGEN_RESTRICT dst, Index C_stride_row, Index C_stride_col,
Scalar alpha, Index row_start, int pw, Index col_start, int cw, Index rows,
Index cols) __arm_streaming __arm_inout("za") {
sme_store_za_pair<RealScalar, kTileRe, kTileIm, FoldRealAlpha>(dst, C_stride_row, C_stride_col, alpha, row_start,
pw, col_start, cw, rows, cols);
}
};
template <typename Scalar, int R, int C, bool ConjLhs, bool ConjRhs>
struct sme_complex_cell<Scalar, R, C, ConjLhs, ConjRhs, false> {
using Vec = typename sme_packet_traits<typename NumTraits<Scalar>::Real>::type;
static EIGEN_ALWAYS_INLINE void accumulate(svbool_t, svbool_t, Vec, Vec, Vec, Vec) __arm_streaming __arm_inout("za") {
}
template <bool FoldRealAlpha, typename Index>
static EIGEN_ALWAYS_INLINE void store(Scalar*, Index, Index, Scalar, Index, int, Index, int, Index,
Index) __arm_streaming __arm_inout("za") {}
};
/*****************************************************************************
* sme_process -- micro-kernel for one pw x cw output block.
*
* Tiles the block into svl x svl ZA tiles, processed in passes of up to a 2x2
* tile grid: several (2*svl) x (2*svl) sub-block passes when the grid is
* smaller than the block, tiles predicated down to the block width when it is
* larger. blA/blB are packed depth-major with depth-strides pw and cw
* respectively.
*
* When the block matches the tile grid exactly (pw == cw == 2 * svl), the
* packed rows are also contiguous across depth steps, enabling the
* hand-scheduled loop below: per 4 unrolled depth steps, 2 x4 loads per
* side (each spanning 2 depth steps) feed 16 FMOPAs -- a 1:1 compute:load
* ratio at the vector level. All other geometries use predicated
* per-depth-step loads.
*****************************************************************************/
template <bool ConjLhs, bool ConjRhs, typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_process(Scalar* EIGEN_RESTRICT C, Index C_stride_row, Index C_stride_col,
const Scalar* EIGEN_RESTRICT blA, const Scalar* EIGEN_RESTRICT blB, Index depth,
Scalar alpha, Index row_start, int pw, Index col_start, int cw,
Index a_step) __arm_streaming __arm_inout("za") {
// Conjugation is the identity on real scalars, so this overload ignores it.
// a_step is the distance between A's depth steps: pw for a packed panel, the
// column stride for a ColMajor source read in place.
using Traits = sme_packet_traits<Scalar>;
using Vec = typename Traits::type;
const int svl = Traits::size();
for (int rt = 0; rt < pw; rt += 2 * svl) {
const int rpw = sme_min(pw - rt, 2 * svl);
const int rlo = sme_min(rpw, svl);
const int rhi = rpw - rlo; // >= 0; > 0 only when rpw > svl, in which case rlo == svl
const svbool_t pg_rlo = Traits::whilelt(rt, pw);
const svbool_t pg_rhi = Traits::whilelt(rt + svl, pw);
for (int ct = 0; ct < cw; ct += 2 * svl) {
const int cpw = sme_min(cw - ct, 2 * svl);
const int clo = sme_min(cpw, svl);
const int chi = cpw - clo;
const svbool_t pg_clo = Traits::whilelt(ct, cw);
const svbool_t pg_chi = Traits::whilelt(ct + svl, cw);
svzero_za();
if (pw == 2 * svl && cw == 2 * svl) {
// The block is exactly one full-grid patch (single pass, rt == ct ==
// 0, rlo == rhi == clo == chi == svl), so a packed row is the
// patch's slice and rows are contiguous across depth steps: x4 loads
// each span 2 of them, e.g. va_01 = [d0 lo, d0 hi, d1 lo, d1 hi].
const svcount_t pn = Traits::ptrue_c();
const Index depth_4 = (depth / 4) * 4;
Index k = 0;
if (a_step == Index(pw)) {
for (; k < depth_4; k += 4) {
typename Traits::type_x4 va_01 = ploadu_x4(pn, &blA[k * pw]);
typename Traits::type_x4 vb_01 = ploadu_x4(pn, &blB[k * cw]);
// d0
outer_product_2x2<Scalar>(pget<0>(va_01), pget<1>(va_01), pget<0>(vb_01), pget<1>(vb_01));
// d1
outer_product_2x2<Scalar>(pget<2>(va_01), pget<3>(va_01), pget<2>(vb_01), pget<3>(vb_01));
typename Traits::type_x4 va_23 = ploadu_x4(pn, &blA[(k + 2) * pw]);
typename Traits::type_x4 vb_23 = ploadu_x4(pn, &blB[(k + 2) * cw]);
// d2
outer_product_2x2<Scalar>(pget<0>(va_23), pget<1>(va_23), pget<0>(vb_23), pget<1>(vb_23));
// d3
outer_product_2x2<Scalar>(pget<2>(va_23), pget<3>(va_23), pget<2>(vb_23), pget<3>(vb_23));
}
} else {
// A read in place: its depth steps are a_step apart, one x2 load each.
for (; k < depth_4; k += 4) {
typename Traits::type_x2 va_0 = ploadu_x2(pn, &blA[k * a_step]);
typename Traits::type_x2 va_1 = ploadu_x2(pn, &blA[(k + 1) * a_step]);
typename Traits::type_x4 vb_01 = ploadu_x4(pn, &blB[k * cw]);
outer_product_2x2<Scalar>(pget<0>(va_0), pget<1>(va_0), pget<0>(vb_01), pget<1>(vb_01));
outer_product_2x2<Scalar>(pget<0>(va_1), pget<1>(va_1), pget<2>(vb_01), pget<3>(vb_01));
typename Traits::type_x2 va_2 = ploadu_x2(pn, &blA[(k + 2) * a_step]);
typename Traits::type_x2 va_3 = ploadu_x2(pn, &blA[(k + 3) * a_step]);
typename Traits::type_x4 vb_23 = ploadu_x4(pn, &blB[(k + 2) * cw]);
outer_product_2x2<Scalar>(pget<0>(va_2), pget<1>(va_2), pget<0>(vb_23), pget<1>(vb_23));
outer_product_2x2<Scalar>(pget<0>(va_3), pget<1>(va_3), pget<2>(vb_23), pget<3>(vb_23));
}
}
// Depth tail: one x2 load per side per step.
for (; k < depth; ++k) {
typename Traits::type_x2 va = ploadu_x2(pn, &blA[k * a_step]);
typename Traits::type_x2 vb = ploadu_x2(pn, &blB[k * cw]);
outer_product_2x2<Scalar>(pget<0>(va), pget<1>(va), pget<0>(vb), pget<1>(vb));
}
} else {
for (Index k = 0; k < depth; ++k) {
Vec a_lo = ploadu(pg_rlo, &blA[k * a_step + rt]);
Vec b_lo = ploadu(pg_clo, &blB[k * cw + ct]);
Vec a_hi = ploadu(pg_rhi, sme_offset(blA, k * a_step + rt + svl));
Vec b_hi = ploadu(pg_chi, sme_offset(blB, k * cw + ct + svl));
sme_mopa<0>(pg_rlo, pg_clo, a_lo, b_lo);
if (svptest_any(pg_chi, pg_chi)) sme_mopa<1>(pg_rlo, pg_chi, a_lo, b_hi);
if (svptest_any(pg_rhi, pg_rhi)) {
sme_mopa<2>(pg_rhi, pg_clo, a_hi, b_lo);
if (svptest_any(pg_chi, pg_chi)) sme_mopa<3>(pg_rhi, pg_chi, a_hi, b_hi);
}
}
}
// Store the (up to) 2x2 grid of tiles for this sub-block pass.
sme_store_2x2_grid(C, C_stride_row, C_stride_col, alpha, row_start + rt, rlo, rhi, col_start + ct, clo, chi);
}
}
}
/*****************************************************************************
* sme_store_complex_grid -- store the (up to) 2x2 grid of tile pairs, exactly
* as sme_store_2x2_grid does for single tiles. Cells outside the grid are
* dropped at compile time by sme_complex_cell, so a narrower grid simply never
* reaches them (its hi widths are structurally zero).
*****************************************************************************/
template <typename Scalar, bool ConjLhs, bool ConjRhs, bool FoldRealAlpha, typename Index>
EIGEN_ALWAYS_INLINE void sme_store_complex_grid(Scalar* EIGEN_RESTRICT C, Index C_stride_row, Index C_stride_col,
Scalar alpha, Index row_start, int rlo, int rhi, Index col_start,
int clo, int chi, Index rows,
Index cols) __arm_streaming __arm_inout("za") {
const int svl = sme_packet_traits<typename NumTraits<Scalar>::Real>::size();
sme_complex_cell<Scalar, 0, 0, ConjLhs, ConjRhs>::template store<FoldRealAlpha>(
C, C_stride_row, C_stride_col, alpha, row_start, rlo, col_start, clo, rows, cols);
if (chi > 0) {
sme_complex_cell<Scalar, 0, 1, ConjLhs, ConjRhs>::template store<FoldRealAlpha>(
C, C_stride_row, C_stride_col, alpha, row_start, rlo, col_start + svl, chi, rows, cols);
}
if (rhi > 0) {
sme_complex_cell<Scalar, 1, 0, ConjLhs, ConjRhs>::template store<FoldRealAlpha>(
C, C_stride_row, C_stride_col, alpha, row_start + svl, rhi, col_start, clo, rows, cols);
if (chi > 0) {
sme_complex_cell<Scalar, 1, 1, ConjLhs, ConjRhs>::template store<FoldRealAlpha>(
C, C_stride_row, C_stride_col, alpha, row_start + svl, rhi, col_start + svl, chi, rows, cols);
}
}
}
/*****************************************************************************
* sme_process, complex overload -- same sub-block structure over a grid of
* complex accumulators, each a pair of ZA tiles (see sme_complex_cell).
*
* The packed panels are read through their real view: one depth step is `pw`
* (`cw`) reals followed by as many imaginary ones, so a cell's four operands
* are four contiguous predicated loads at a fixed offset apart, and the four
* outer products they feed reuse all of them.
*****************************************************************************/
template <bool ConjLhs, bool ConjRhs, bool FoldRealAlpha, typename RealScalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_process(std::complex<RealScalar>* EIGEN_RESTRICT C, Index C_stride_row, Index C_stride_col,
const std::complex<RealScalar>* EIGEN_RESTRICT blA,
const std::complex<RealScalar>* EIGEN_RESTRICT blB, Index depth,
std::complex<RealScalar> alpha, Index row_start, int pw, Index col_start, int cw,
Index lhs_step, Index rows, Index cols) __arm_streaming __arm_inout("za") {
// Complex panels are always packed (split real/imaginary halves): lhs_step is pw.
EIGEN_UNUSED_VARIABLE(lhs_step);
using Scalar = std::complex<RealScalar>;
using Traits = sme_packet_traits<RealScalar>;
using Vec = typename Traits::type;
const int svl = Traits::size();
constexpr int GridRows = sme_block<Scalar>::kGridRows;
constexpr int GridCols = sme_block<Scalar>::kGridCols;
using Cell00 = sme_complex_cell<Scalar, 0, 0, ConjLhs, ConjRhs>;
using Cell01 = sme_complex_cell<Scalar, 0, 1, ConjLhs, ConjRhs>;
using Cell10 = sme_complex_cell<Scalar, 1, 0, ConjLhs, ConjRhs>;
using Cell11 = sme_complex_cell<Scalar, 1, 1, ConjLhs, ConjRhs>;
const RealScalar* EIGEN_RESTRICT rA = reinterpret_cast<const RealScalar*>(blA);
const RealScalar* EIGEN_RESTRICT rB = reinterpret_cast<const RealScalar*>(blB);
const Index a_step = Index(2 * pw);
const Index b_step = Index(2 * cw);
for (int rt = 0; rt < pw; rt += GridRows * svl) {
const int rpw = sme_min(pw - rt, GridRows * svl);
const int r0 = sme_min(rpw, svl);
const int r1 = rpw - r0;
const svbool_t pg_r0 = Traits::whilelt(0, rpw);
const svbool_t pg_r1 = Traits::whilelt(svl, rpw);
for (int ct = 0; ct < cw; ct += GridCols * svl) {
const int cpw = sme_min(cw - ct, GridCols * svl);
const int c0 = sme_min(cpw, svl);
const int c1 = cpw - c0;
const svbool_t pg_c0 = Traits::whilelt(0, cpw);
const svbool_t pg_c1 = Traits::whilelt(svl, cpw);
svzero_za();
for (Index k = 0; k < depth; ++k) {
const RealScalar* pa = rA + k * a_step + Index(rt);
const RealScalar* pb = rB + k * b_step + Index(ct);
// The second-tile loads are unconditional: their predicates are empty
// when the grid is a single tile wide or tall, so they touch no memory.
// Grouping them lets the compiler issue them in parallel, and gating the
// outer products on svptest_any keeps the depth loop unswitched without
// materialising the r1/c1 counts here.
const Vec a0_re = ploadu(pg_r0, pa);
const Vec a0_im = ploadu(pg_r0, pa + pw);
const Vec b0_re = ploadu(pg_c0, pb);
const Vec b0_im = ploadu(pg_c0, pb + cw);
const Vec a1_re = ploadu(pg_r1, sme_offset(pa, Index(svl)));
const Vec a1_im = ploadu(pg_r1, sme_offset(pa, Index(pw) + Index(svl)));
const Vec b1_re = ploadu(pg_c1, sme_offset(pb, Index(svl)));
const Vec b1_im = ploadu(pg_c1, sme_offset(pb, Index(cw) + Index(svl)));
Cell00::accumulate(pg_r0, pg_c0, a0_re, a0_im, b0_re, b0_im);
if (svptest_any(pg_c1, pg_c1)) {
Cell01::accumulate(pg_r0, pg_c1, a0_re, a0_im, b1_re, b1_im);
}
if (svptest_any(pg_r1, pg_r1)) {
Cell10::accumulate(pg_r1, pg_c0, a1_re, a1_im, b0_re, b0_im);
if (svptest_any(pg_c1, pg_c1)) {
Cell11::accumulate(pg_r1, pg_c1, a1_re, a1_im, b1_re, b1_im);
}
}
}
sme_store_complex_grid<Scalar, ConjLhs, ConjRhs, FoldRealAlpha>(
C, C_stride_row, C_stride_col, alpha, row_start + rt, r0, r1, col_start + ct, c0, c1, rows, cols);
}
}
}
// A RHS panel of at most svl columns fills only one tile column of the 2 x 2 grid, so the four tiles are stacked along
// M instead: a full LHS panel (tiles 0 and 1) and the next one (tiles 2 and 3, pw1 rows) against the same B vector.
template <typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_process_narrow(Scalar* EIGEN_RESTRICT C, Index C_stride_row, Index C_stride_col,
const Scalar* EIGEN_RESTRICT blA0, Index step0,
const Scalar* EIGEN_RESTRICT blA1, Index step1, int pw1,
const Scalar* EIGEN_RESTRICT blB, int cw, Index depth, Scalar alpha,
Index row_start, Index col_start) __arm_streaming __arm_inout("za") {
using Traits = sme_packet_traits<Scalar>;
using Vec = typename Traits::type;
const int svl = Traits::size();
const svcount_t pc = Traits::ptrue_c();
const svbool_t all = Traits::ptrue();
const svbool_t pn = Traits::whilelt(0, cw);
const svbool_t p1lo = Traits::whilelt(0, pw1);
const svbool_t p1hi = Traits::whilelt(svl, pw1);
svzero_za();
for (Index k = 0; k < depth; ++k) {
const auto a0 = ploadu_x2(pc, blA0 + k * step0);
const Vec a1lo = ploadu(p1lo, blA1 + k * step1);
const Vec a1hi = ploadu(p1hi, sme_offset(blA1, k * step1 + svl));
const Vec b = ploadu(pn, blB + k * cw);
sme_mopa<0>(all, pn, pget<0>(a0), b);
sme_mopa<1>(all, pn, pget<1>(a0), b);
sme_mopa<2>(p1lo, pn, a1lo, b);
sme_mopa<3>(p1hi, pn, a1hi, b);
}
sme_store_za_tile<Scalar, 0>(C, C_stride_row, C_stride_col, alpha, row_start, svl, col_start, cw);
sme_store_za_tile<Scalar, 1>(C, C_stride_row, C_stride_col, alpha, row_start + svl, svl, col_start, cw);
sme_store_za_tile<Scalar, 2>(C, C_stride_row, C_stride_col, alpha, row_start + 2 * svl, sme_min(pw1, svl), col_start,
cw);
if (pw1 > svl)
sme_store_za_tile<Scalar, 3>(C, C_stride_row, C_stride_col, alpha, row_start + 3 * svl, pw1 - svl, col_start, cw);
}
// Tile Dst += tile Src, four columns per step.
template <int Dst, int Src, typename Scalar>
EIGEN_ALWAYS_INLINE void sme_fold_tile() __arm_streaming __arm_inout("za") {
const svbool_t all = sme_packet_traits<Scalar>::ptrue();
for (int c = 0; c < sme_packet_traits<Scalar>::size(); c += 4) {
const auto d = sme_read_ver_za_vg4<Dst, Scalar>(uint32_t(c)), s = sme_read_ver_za_vg4<Src, Scalar>(uint32_t(c));
sme_write_ver_za_vg4<Dst>(uint32_t(c),
pcreate(padd(all, pget<0>(d), pget<0>(s)), padd(all, pget<1>(d), pget<1>(s)),
padd(all, pget<2>(d), pget<2>(s)), padd(all, pget<3>(d), pget<3>(s))));
}
}
// A block that fills one or two tiles (pw or cw <= svl): FMOPAs into one tile wait on each other, so consecutive
// depth steps go to the spare tiles and the partial sums are folded before the store.
template <typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_process_split(Scalar* EIGEN_RESTRICT C, Index C_stride_row, Index C_stride_col,
const Scalar* EIGEN_RESTRICT blA, const Scalar* EIGEN_RESTRICT blB,
Index depth, Scalar alpha, Index row_start, int pw, Index col_start, int cw,
Index a_step) __arm_streaming __arm_inout("za") {
using Traits = sme_packet_traits<Scalar>;
using Vec = typename Traits::type;
const int svl = Traits::size();
const svbool_t pr0 = Traits::whilelt(0, pw), pr1 = Traits::whilelt(svl, pw);
const svbool_t pc0 = Traits::whilelt(0, cw), pc1 = Traits::whilelt(svl, cw);
svzero_za();
Index k = 0;
if (pw <= svl && cw <= svl) {
for (; k + 4 <= depth; k += 4) {
const Vec a0 = ploadu(pr0, blA + k * a_step), a1 = ploadu(pr0, blA + (k + 1) * a_step);
const Vec a2 = ploadu(pr0, blA + (k + 2) * a_step), a3 = ploadu(pr0, blA + (k + 3) * a_step);
const Vec b0 = ploadu(pc0, blB + k * cw), b1 = ploadu(pc0, blB + (k + 1) * cw);
const Vec b2 = ploadu(pc0, blB + (k + 2) * cw), b3 = ploadu(pc0, blB + (k + 3) * cw);
sme_mopa<0>(pr0, pc0, a0, b0);
sme_mopa<1>(pr0, pc0, a1, b1);
sme_mopa<2>(pr0, pc0, a2, b2);
sme_mopa<3>(pr0, pc0, a3, b3);
}
for (; k < depth; ++k) sme_mopa<0>(pr0, pc0, ploadu(pr0, blA + k * a_step), ploadu(pc0, blB + k * cw));
sme_fold_tile<0, 1, Scalar>();
sme_fold_tile<2, 3, Scalar>();
sme_fold_tile<0, 2, Scalar>();
sme_store_za_tile<Scalar, 0>(C, C_stride_row, C_stride_col, alpha, row_start, pw, col_start, cw);
} else if (pw <= svl) {
// Tiles 0 and 1 take the even depth steps, 2 and 3 the odd ones.
for (; k + 2 <= depth; k += 2) {
const Vec a0 = ploadu(pr0, blA + k * a_step), a1 = ploadu(pr0, blA + (k + 1) * a_step);
const Vec b0 = ploadu(pc0, blB + k * cw), b0h = ploadu(pc1, blB + k * cw + svl);
const Vec b1 = ploadu(pc0, blB + (k + 1) * cw), b1h = ploadu(pc1, blB + (k + 1) * cw + svl);
sme_mopa<0>(pr0, pc0, a0, b0);
sme_mopa<1>(pr0, pc1, a0, b0h);
sme_mopa<2>(pr0, pc0, a1, b1);
sme_mopa<3>(pr0, pc1, a1, b1h);
}
if (k < depth) {
const Vec a0 = ploadu(pr0, blA + k * a_step);
sme_mopa<0>(pr0, pc0, a0, ploadu(pc0, blB + k * cw));
sme_mopa<1>(pr0, pc1, a0, ploadu(pc1, blB + k * cw + svl));
}
sme_fold_tile<0, 2, Scalar>();
sme_fold_tile<1, 3, Scalar>();
sme_store_2x2_grid(C, C_stride_row, C_stride_col, alpha, row_start, pw, 0, col_start, svl, cw - svl);
} else {
// cw <= svl < pw: tiles 0 and 2 take the even depth steps, 1 and 3 the odd ones.
for (; k + 2 <= depth; k += 2) {
const Vec a0 = ploadu(pr0, blA + k * a_step), a0h = ploadu(pr1, blA + k * a_step + svl);
const Vec a1 = ploadu(pr0, blA + (k + 1) * a_step), a1h = ploadu(pr1, blA + (k + 1) * a_step + svl);
const Vec b0 = ploadu(pc0, blB + k * cw), b1 = ploadu(pc0, blB + (k + 1) * cw);
sme_mopa<0>(pr0, pc0, a0, b0);
sme_mopa<2>(pr1, pc0, a0h, b0);
sme_mopa<1>(pr0, pc0, a1, b1);
sme_mopa<3>(pr1, pc0, a1h, b1);
}
if (k < depth) {
const Vec b0 = ploadu(pc0, blB + k * cw);
sme_mopa<0>(pr0, pc0, ploadu(pr0, blA + k * a_step), b0);
sme_mopa<2>(pr1, pc0, ploadu(pr1, blA + k * a_step + svl), b0);
}
sme_fold_tile<0, 1, Scalar>();
sme_fold_tile<2, 3, Scalar>();
sme_store_2x2_grid(C, C_stride_row, C_stride_col, alpha, row_start, svl, pw - svl, col_start, cw, 0);
}
}
// One pw x cw block: sme_process_split when it fills at most two tiles of the 2 x 2 grid; complex blocks keep
// sme_process. `split_ok` (sme_split_ok) holds once per call, outside the block loops.
template <bool ConjLhs, bool ConjRhs, bool FoldRealAlpha, typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_process_block(Scalar* C, Index rs, Index cs, const Scalar* blA, const Scalar* blB,
Index depth, Scalar alpha, Index row_start, int pw, Index col_start, int cw,
Index a_step, bool split_ok, Index,
Index) __arm_streaming __arm_inout("za") {
const int svl = sme_packet_traits<Scalar>::size();
if (split_ok && (pw <= svl || cw <= svl))
sme_process_split(C, rs, cs, blA, blB, depth, alpha, row_start, pw, col_start, cw, a_step);
else
sme_process<ConjLhs, ConjRhs>(C, rs, cs, blA, blB, depth, alpha, row_start, pw, col_start, cw, a_step);
}
template <bool ConjLhs, bool ConjRhs, bool FoldRealAlpha, typename RealScalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_process_block(std::complex<RealScalar>* C, Index rs, Index cs,
const std::complex<RealScalar>* blA, const std::complex<RealScalar>* blB,
Index depth, std::complex<RealScalar> alpha, Index row_start, int pw,
Index col_start, int cw, Index a_step, bool, Index rows,
Index cols) __arm_streaming __arm_inout("za") {
sme_process<ConjLhs, ConjRhs, FoldRealAlpha>(C, rs, cs, blA, blB, depth, alpha, row_start, pw, col_start, cw, a_step,
rows, cols);
}
// Whether the vector length gives the tile shapes the block kernels assume: the depth-split kernel covers one 2 x 2
// grid of blocks up to MR x NR with four-slice tile folds, and the narrow kernel stacks two LHS panels of 2*svl rows.
template <typename Scalar>
EIGEN_ALWAYS_INLINE bool sme_split_ok() __arm_streaming {
const int svl = sme_packet_traits<typename NumTraits<Scalar>::Real>::size();
return svl >= 4 && sme_block<Scalar>::mr <= 2 * svl && sme_block<Scalar>::nr <= 2 * svl;
}
template <typename Scalar>
EIGEN_ALWAYS_INLINE bool sme_narrow_ok() __arm_streaming {
return sme_block<Scalar>::mr == 2 * sme_packet_traits<typename NumTraits<Scalar>::Real>::size();
}
// sme_process_narrow for real scalars; complex panels keep the 2 x 2 path.
template <typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_process_narrow_dispatch(Scalar* C, Index rs, Index cs, const Scalar* blA0, Index step0,
const Scalar* blA1, Index step1, int pw1, const Scalar* blB,
int cw, Index depth, Scalar alpha, Index row_start,
Index col_start) __arm_streaming __arm_inout("za") {
sme_process_narrow(C, rs, cs, blA0, step0, blA1, step1, pw1, blB, cw, depth, alpha, row_start, col_start);
}
template <typename RealScalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_process_narrow_dispatch(std::complex<RealScalar>*, Index, Index,
const std::complex<RealScalar>*, Index,
const std::complex<RealScalar>*, Index, int,
const std::complex<RealScalar>*, int, Index,
std::complex<RealScalar>, Index,
Index) __arm_streaming __arm_inout("za") {}
// Core-side prefetch into L2 of the column-major C block the next sme_process call reads and writes.
template <typename Scalar, typename Index>
static EIGEN_ALWAYS_INLINE void sme_prefetch_next_c(const Scalar* C, Index C_stride_row, Index C_stride_col, Index i,
Index j, Index rows, Index cols, int mr,
int nr) __arm_streaming_compatible {
if (C_stride_row != 1 || j >= cols) return;
const Index h = sme_min(rows - i, Index(mr)), w = sme_min(cols - j, Index(nr));
const Index bytes = h * Index(sizeof(Scalar));
for (Index c = 0; c < w; ++c)
for (Index b = 0; b < bytes; b += 128)
__builtin_prefetch(reinterpret_cast<const char*>(C + i + (j + c) * C_stride_col) + b, 1, 2);
}
template <typename Scalar, bool ConjLhs, bool ConjRhs, bool FoldRealAlpha = false, typename Index>
EIGEN_DONT_INLINE __arm_locally_streaming __arm_new("za") void sme_gebp_impl(
Scalar* C, Index C_stride_row, Index C_stride_col, const Scalar* blockA, const Scalar* blockB, Index rows,
Index depth, Index cols, Scalar alpha, Index strideA, Index strideB, Index offsetA, Index offsetB) {
constexpr int MR = sme_block<Scalar>::mr;
constexpr int NR = sme_block<Scalar>::nr;
// Column-outer, row-inner: keeps blB (one kc × NR panel) hot in L1 while
// smaller blA tiles stream from L2. The outer GOTO loop in
// GeneralMatrixMatrix.h ensures blockA fits in L2 via mc-blocking. Each
// packed panel is depth-major with depth-stride equal to its width (MR/NR
// for full panels, the tail width otherwise), so that width is passed as
// both the logical block size and the load stride to the block kernels.
// The narrow kernel stacks two LHS panels of exactly 2*svl rows.
// A small C block (256 KB or less) gains nothing from the prefetch and pays its instructions on every call.
const bool prefetch_c = rows * cols * Index(sizeof(Scalar)) > Index(256 * 1024);
const bool split_ok = sme_split_ok<Scalar>(), narrow_ok = sme_narrow_ok<Scalar>();
for (Index j = 0; j < cols; j += NR) {
const int cw = static_cast<int>(sme_min(cols - j, Index(NR)));
const Scalar* blB = blockB + j * strideB + offsetB * cw;
Index i = 0;
EIGEN_IF_CONSTEXPR (!NumTraits<Scalar>::IsComplex) {
if (narrow_ok && cw <= sme_packet_traits<typename NumTraits<Scalar>::Real>::size())
for (; i + MR < rows; i += 2 * MR) {
const int pw1 = static_cast<int>(sme_min(rows - i - MR, Index(MR)));
sme_process_narrow_dispatch(C, C_stride_row, C_stride_col, blockA + i * strideA + offsetA * MR, Index(MR),
blockA + (i + MR) * strideA + offsetA * pw1, Index(pw1), pw1, blB, cw, depth,
alpha, i, j);
}
}
for (; i < rows; i += MR) {
const int pw = static_cast<int>(sme_min(rows - i, Index(MR)));
const Scalar* blA = blockA + i * strideA + offsetA * pw;
if (prefetch_c)
sme_prefetch_next_c(C, C_stride_row, C_stride_col, i + MR < rows ? i + MR : Index(0),
i + MR < rows ? j : j + NR, rows, cols, MR, NR);
sme_process_block<ConjLhs, ConjRhs, FoldRealAlpha>(C, C_stride_row, C_stride_col, blA, blB, depth, alpha, i, pw,
j, cw, Index(pw), split_ok, rows, cols);
}
}
}
// Picks the sme_gebp_impl instantiation. A complex product with a real alpha
// other than 1, e.g. the -1 of C.noalias() -= A*B, gets the kernel that folds
// C into one FMA per component (see sme_accumulate_pair_real_alpha); every
// other product gets the kernel without that path. Real scalars have one.
template <typename Scalar, bool ConjLhs, bool ConjRhs, bool IsComplex = NumTraits<Scalar>::IsComplex>
struct sme_gebp_dispatch {
template <typename Index>
static void run(Scalar* C, Index C_stride_row, Index C_stride_col, const Scalar* blockA, const Scalar* blockB,
Index rows, Index depth, Index cols, Scalar alpha, Index strideA, Index strideB, Index offsetA,
Index offsetB) {
sme_gebp_impl<Scalar, ConjLhs, ConjRhs, false>(C, C_stride_row, C_stride_col, blockA, blockB, rows, depth, cols,
alpha, strideA, strideB, offsetA, offsetB);
}
};
template <typename Scalar, bool ConjLhs, bool ConjRhs>
struct sme_gebp_dispatch<Scalar, ConjLhs, ConjRhs, true> {
template <typename Index>
static void run(Scalar* C, Index C_stride_row, Index C_stride_col, const Scalar* blockA, const Scalar* blockB,
Index rows, Index depth, Index cols, Scalar alpha, Index strideA, Index strideB, Index offsetA,
Index offsetB) {
using RealScalar = typename NumTraits<Scalar>::Real;
if (numext::imag(alpha) == RealScalar(0) && numext::real(alpha) != RealScalar(1)) {
sme_gebp_impl<Scalar, ConjLhs, ConjRhs, true>(C, C_stride_row, C_stride_col, blockA, blockB, rows, depth, cols,
alpha, strideA, strideB, offsetA, offsetB);
} else {
sme_gebp_impl<Scalar, ConjLhs, ConjRhs, false>(C, C_stride_row, C_stride_col, blockA, blockB, rows, depth, cols,
alpha, strideA, strideB, offsetA, offsetB);
}
}
};
// gebp with the LHS read from a ColMajor source: rows i..i+pw of column k are
// at lhs + i + k * lda, the packed layout with a_step = lda. Real scalars only.
template <typename Scalar, typename Index>
EIGEN_DONT_INLINE __arm_locally_streaming __arm_new("za") void sme_gebp_impl_direct_lhs(
Scalar* C, Index C_stride_row, Index C_stride_col, const Scalar* lhs, Index lda, const Scalar* blockB, Index rows,
Index depth, Index cols, Scalar alpha, Index strideB, Index offsetB) {
constexpr int MR = sme_block<Scalar>::mr;
constexpr int NR = sme_block<Scalar>::nr;
const bool split_ok = sme_split_ok<Scalar>(), narrow_ok = sme_narrow_ok<Scalar>();
for (Index j = 0; j < cols; j += NR) {
const int cw = static_cast<int>(sme_min(cols - j, Index(NR)));
const Scalar* blB = blockB + j * strideB + offsetB * cw;
Index i = 0;
if (narrow_ok && cw <= sme_packet_traits<Scalar>::size())
for (; i + MR < rows; i += 2 * MR)
sme_process_narrow(C, C_stride_row, C_stride_col, lhs + i, lda, lhs + i + MR, lda,
static_cast<int>(sme_min(rows - i - MR, Index(MR))), blB, cw, depth, alpha, i, j);
for (; i < rows; i += MR) {
const int pw = static_cast<int>(sme_min(rows - i, Index(MR)));
sme_process_block<false, false, false>(C, C_stride_row, C_stride_col, lhs + i, blB, depth, alpha, i, pw, j, cw,
lda, split_ok, rows, cols);
}
}
}
// In-place ColMajor LHS: deep enough for the ZA kernel, a stride that is not a 4 KB multiple (such columns map to one
// L1 set) and a block of at most 4 MB, past which the strided re-reads lose to the packed panel.
#ifndef EIGEN_SME_DIRECT_LHS_MAX_STRIDE_BYTES
#define EIGEN_SME_DIRECT_LHS_MAX_STRIDE_BYTES 16384
#endif
#ifndef EIGEN_SME_DIRECT_LHS_MAX_BLOCK_BYTES
#define EIGEN_SME_DIRECT_LHS_MAX_BLOCK_BYTES (4 << 20)
#endif
#ifndef EIGEN_SME_DIRECT_LHS_MAX_SPAN_BYTES
#define EIGEN_SME_DIRECT_LHS_MAX_SPAN_BYTES (16 << 20)
#endif
template <typename Scalar, typename Index>
bool sme_direct_lhs_ok(Index lhsStride, Index rows, Index depth, Index cols) {
#ifdef EIGEN_SME_FORCE_NEON_SMALL_BLOCKS
EIGEN_UNUSED_VARIABLE(lhsStride);
EIGEN_UNUSED_VARIABLE(rows);
EIGEN_UNUSED_VARIABLE(depth);
EIGEN_UNUSED_VARIABLE(cols);
return false;
#else
if (NumTraits<Scalar>::IsComplex || depth <= Index(sme_neon_max_depth<Scalar>::value)) return false;
// A RHS of one panel reads each LHS element once, so packing it only adds a copy: always for the narrow kernel,
// and for the 2 x 2 one while a panel spans few enough pages.
const std::size_t stride_bytes = std::size_t(lhsStride) * sizeof(Scalar);
if (cols <= Index(sme_block<Scalar>::nr)) {
const std::size_t span_limit = std::size_t(EIGEN_SME_DIRECT_LHS_MAX_SPAN_BYTES);
if (span_limit == 0) return false;
return cols <= Index(sme_block<Scalar>::nr / 2) || std::size_t(depth) * stride_bytes <= span_limit;
}
return stride_bytes % 4096 != 0 && stride_bytes <= std::size_t(EIGEN_SME_DIRECT_LHS_MAX_STRIDE_BYTES) &&
std::size_t(rows) * std::size_t(depth) * sizeof(Scalar) <= std::size_t(EIGEN_SME_DIRECT_LHS_MAX_BLOCK_BYTES);
#endif
}
// NEON path for small blocks: a shallow block pays the ZA enable and a dependent FMOPA chain per
// tile for little work, so its panels are consumed with NEON in the SME layout (one depth step is
// pw contiguous scalars, complex ones as pw reals then pw imaginaries).
// The NCol columns of one depth step, loaded once; each column is then an
// immediate lane of a fused multiply-add (nmadd: acc - a * b[lane]).
template <typename Scalar, int NCol>
struct sme_neon_cols;
template <>
struct sme_neon_cols<float, 4> {
using Vec = float32x4_t;
static EIGEN_ALWAYS_INLINE Vec load(const float* b) { return vld1q_f32(b); }
template <int C>
static EIGEN_ALWAYS_INLINE Packet4f madd(Packet4f acc, Packet4f a, Vec b) {
return vfmaq_laneq_f32(acc, a, b, C);
}
template <int C>
static EIGEN_ALWAYS_INLINE Packet4f nmadd(Packet4f acc, Packet4f a, Vec b) {
return vfmsq_laneq_f32(acc, a, b, C);
}
};
template <>
struct sme_neon_cols<float, 2> {
using Vec = float32x2_t;
static EIGEN_ALWAYS_INLINE Vec load(const float* b) { return vld1_f32(b); }
template <int C>
static EIGEN_ALWAYS_INLINE Packet4f madd(Packet4f acc, Packet4f a, Vec b) {
return vfmaq_lane_f32(acc, a, b, C);
}
template <int C>
static EIGEN_ALWAYS_INLINE Packet4f nmadd(Packet4f acc, Packet4f a, Vec b) {
return vfmsq_lane_f32(acc, a, b, C);
}
};
template <>
struct sme_neon_cols<float, 1> {
using Vec = float32x4_t;
static EIGEN_ALWAYS_INLINE Vec load(const float* b) { return vld1q_dup_f32(b); }
template <int C>
static EIGEN_ALWAYS_INLINE Packet4f madd(Packet4f acc, Packet4f a, Vec b) {
return vfmaq_f32(acc, a, b);
}
template <int C>
static EIGEN_ALWAYS_INLINE Packet4f nmadd(Packet4f acc, Packet4f a, Vec b) {
return vfmsq_f32(acc, a, b);
}
};
template <>
struct sme_neon_cols<double, 4> {
struct Vec {
float64x2_t lo, hi;
};
static EIGEN_ALWAYS_INLINE Vec load(const double* b) { return {vld1q_f64(b), vld1q_f64(b + 2)}; }
template <int C>
static EIGEN_ALWAYS_INLINE Packet2d madd(Packet2d acc, Packet2d a, Vec b) {
return vfmaq_laneq_f64(acc, a, C < 2 ? b.lo : b.hi, C & 1);
}
template <int C>
static EIGEN_ALWAYS_INLINE Packet2d nmadd(Packet2d acc, Packet2d a, Vec b) {
return vfmsq_laneq_f64(acc, a, C < 2 ? b.lo : b.hi, C & 1);
}
};
template <>
struct sme_neon_cols<double, 2> {
using Vec = float64x2_t;
static EIGEN_ALWAYS_INLINE Vec load(const double* b) { return vld1q_f64(b); }
template <int C>
static EIGEN_ALWAYS_INLINE Packet2d madd(Packet2d acc, Packet2d a, Vec b) {
return vfmaq_laneq_f64(acc, a, b, C);
}
template <int C>
static EIGEN_ALWAYS_INLINE Packet2d nmadd(Packet2d acc, Packet2d a, Vec b) {
return vfmsq_laneq_f64(acc, a, b, C);
}
};
template <>
struct sme_neon_cols<double, 1> {
using Vec = float64x2_t;
static EIGEN_ALWAYS_INLINE Vec load(const double* b) { return vld1q_dup_f64(b); }
template <int C>
static EIGEN_ALWAYS_INLINE Packet2d madd(Packet2d acc, Packet2d a, Vec b) {
return vfmaq_f64(acc, a, b);
}
template <int C>
static EIGEN_ALWAYS_INLINE Packet2d nmadd(Packet2d acc, Packet2d a, Vec b) {
return vfmsq_f64(acc, a, b);
}
};
template <>
struct sme_neon_cols<float, 8> {
struct Vec {
float32x4_t lo, hi;
};
static EIGEN_ALWAYS_INLINE Vec load(const float* b) { return {vld1q_f32(b), vld1q_f32(b + 4)}; }
template <int C>
static EIGEN_ALWAYS_INLINE Packet4f madd(Packet4f acc, Packet4f a, Vec b) {
return vfmaq_laneq_f32(acc, a, C < 4 ? b.lo : b.hi, C & 3);
}
template <int C>
static EIGEN_ALWAYS_INLINE Packet4f nmadd(Packet4f acc, Packet4f a, Vec b) {
return vfmsq_laneq_f32(acc, a, C < 4 ? b.lo : b.hi, C & 3);
}
};
template <>
struct sme_neon_cols<double, 8> {
struct Vec {
float64x2_t v[4];
};
static EIGEN_ALWAYS_INLINE Vec load(const double* b) {
return {{vld1q_f64(b), vld1q_f64(b + 2), vld1q_f64(b + 4), vld1q_f64(b + 6)}};
}
template <int C>
static EIGEN_ALWAYS_INLINE Packet2d madd(Packet2d acc, Packet2d a, Vec b) {
return vfmaq_laneq_f64(acc, a, b.v[C >> 1], C & 1);
}
template <int C>
static EIGEN_ALWAYS_INLINE Packet2d nmadd(Packet2d acc, Packet2d a, Vec b) {
return vfmsq_laneq_f64(acc, a, b.v[C >> 1], C & 1);
}
};
// Compile-time loop over the columns of a tile (lane numbers are immediates).
template <int C, int NCol>
struct sme_neon_col_loop {
template <typename Cols, typename Packet, int NPack>
static EIGEN_ALWAYS_INLINE void real(Packet (&acc)[NPack][NCol], const Packet (&av)[NPack], typename Cols::Vec b) {
for (int p = 0; p < NPack; ++p) acc[p][C] = Cols::template madd<C>(acc[p][C], av[p], b);
sme_neon_col_loop<C + 1, NCol>::template real<Cols, Packet, NPack>(acc, av, b);
}
template <bool ConjLhs, bool ConjRhs, typename Cols, typename Packet, int NPack>
static EIGEN_ALWAYS_INLINE void cplx(Packet (&acc_re)[NPack][NCol], Packet (&acc_im)[NPack][NCol],
const Packet (&are)[NPack], const Packet (&aim)[NPack], typename Cols::Vec bre,
typename Cols::Vec bim) {
for (int p = 0; p < NPack; ++p) {
acc_re[p][C] = Cols::template madd<C>(acc_re[p][C], are[p], bre);
acc_re[p][C] = (ConjLhs != ConjRhs) ? Cols::template madd<C>(acc_re[p][C], aim[p], bim)
: Cols::template nmadd<C>(acc_re[p][C], aim[p], bim);
acc_im[p][C] = ConjLhs ? Cols::template nmadd<C>(acc_im[p][C], aim[p], bre)
: Cols::template madd<C>(acc_im[p][C], aim[p], bre);
acc_im[p][C] = ConjRhs ? Cols::template nmadd<C>(acc_im[p][C], are[p], bim)
: Cols::template madd<C>(acc_im[p][C], are[p], bim);
}
sme_neon_col_loop<C + 1, NCol>::template cplx<ConjLhs, ConjRhs, Cols, Packet, NPack>(acc_re, acc_im, are, aim, bre,
bim);
}
};
template <int NCol>
struct sme_neon_col_loop<NCol, NCol> {
template <typename Cols, typename Packet, int NPack>
static EIGEN_ALWAYS_INLINE void real(Packet (&)[NPack][NCol], const Packet (&)[NPack], typename Cols::Vec) {}
template <bool ConjLhs, bool ConjRhs, typename Cols, typename Packet, int NPack>
static EIGEN_ALWAYS_INLINE void cplx(Packet (&)[NPack][NCol], Packet (&)[NPack][NCol], const Packet (&)[NPack],
const Packet (&)[NPack], typename Cols::Vec, typename Cols::Vec) {}
};
// One (NPack * PacketSize) x NCol micro-tile of a real block; NPack == 0 is a
// single scalar row. Pointers arrive offset to the tile's first row and column.
template <typename Scalar, typename Index, int NPack, int NCol>
struct sme_neon_tile {
using Packet = typename packet_traits<Scalar>::type;
using Cols = sme_neon_cols<Scalar, NCol>;
static constexpr int PS = packet_traits<Scalar>::size;
// Narrow tiles split the depth over KU accumulator chains to hide FMA latency.
static constexpr int KU = (NPack * NCol >= 4) ? 1 : 4 / (NPack * NCol);
static EIGEN_ALWAYS_INLINE void step(Packet (&acc)[NPack][NCol], const Scalar* a, const Scalar* b) {
Packet av[NPack];
for (int p = 0; p < NPack; ++p) av[p] = ploadu<Packet>(a + p * PS);
sme_neon_col_loop<0, NCol>::template real<Cols, Packet, NPack>(acc, av, Cols::load(b));
}
static EIGEN_ALWAYS_INLINE void run(Scalar* C, Index rs, Index cs, const Scalar* blA, const Scalar* blB, Index depth,
Scalar alpha, Index pw, Index cw) {
Packet acc[KU][NPack][NCol];
for (int u = 0; u < KU; ++u)
for (int p = 0; p < NPack; ++p)
for (int c = 0; c < NCol; ++c) acc[u][p][c] = pset1<Packet>(Scalar(0));
Index k = 0;
for (; k + KU <= depth; k += KU) {
for (int u = 0; u < KU; ++u) step(acc[u], blA + (k + u) * pw, blB + (k + u) * cw);
}
for (; k < depth; ++k) step(acc[0], blA + k * pw, blB + k * cw);
for (int u = 1; u < KU; ++u)
for (int p = 0; p < NPack; ++p)
for (int c = 0; c < NCol; ++c) acc[0][p][c] = padd(acc[0][p][c], acc[u][p][c]);
const Packet valpha = pset1<Packet>(alpha);
for (int c = 0; c < NCol; ++c) {
for (int p = 0; p < NPack; ++p) {
Scalar* pc = C + Index(p * PS) * rs + Index(c) * cs;
if (rs == 1) {
pstoreu(pc, pmadd(acc[0][p][c], valpha, ploadu<Packet>(pc)));
} else {
pscatter<Scalar, Packet>(pc, pmadd(acc[0][p][c], valpha, pgather<Scalar, Packet>(pc, rs)), rs);
}
}
}
}
};
template <typename Scalar, typename Index, int NCol>
struct sme_neon_tile<Scalar, Index, 0, NCol> {
static EIGEN_ALWAYS_INLINE void run(Scalar* C, Index rs, Index cs, const Scalar* blA, const Scalar* blB, Index depth,
Scalar alpha, Index pw, Index cw) {
EIGEN_UNUSED_VARIABLE(rs);
constexpr int KU = (NCol >= 4) ? 1 : 4 / NCol;
Scalar acc[KU][NCol];
for (int u = 0; u < KU; ++u)
for (int c = 0; c < NCol; ++c) acc[u][c] = Scalar(0);
Index k = 0;
for (; k + KU <= depth; k += KU) {
for (int u = 0; u < KU; ++u) {
const Scalar a = blA[(k + u) * pw];
const Scalar* b = blB + (k + u) * cw;
for (int c = 0; c < NCol; ++c) acc[u][c] += a * b[c];
}
}
for (; k < depth; ++k) {
const Scalar a = blA[k * pw];
const Scalar* b = blB + k * cw;
for (int c = 0; c < NCol; ++c) acc[0][c] += a * b[c];
}
for (int u = 1; u < KU; ++u)
for (int c = 0; c < NCol; ++c) acc[0][c] += acc[u][c];
for (int c = 0; c < NCol; ++c) C[Index(c) * cs] += alpha * acc[0][c];
}
};
// Complex counterpart over the real view of the panels. Conjugation is folded
// into the signs of the four real products.
template <typename RealScalar, typename Index, bool ConjLhs, bool ConjRhs, int NPack, int NCol>
struct sme_neon_ctile {
using Scalar = std::complex<RealScalar>;
using Packet = typename packet_traits<RealScalar>::type;
using Cols = sme_neon_cols<RealScalar, NCol>;
static constexpr int PS = packet_traits<RealScalar>::size;
static EIGEN_ALWAYS_INLINE void run(Scalar* C, Index rs, Index cs, const RealScalar* rA, const RealScalar* rB,
Index depth, Scalar alpha, Index pw, Index cw) {
Packet acc_re[NPack][NCol], acc_im[NPack][NCol];
for (int p = 0; p < NPack; ++p) {
for (int c = 0; c < NCol; ++c) {
acc_re[p][c] = pset1<Packet>(RealScalar(0));
acc_im[p][c] = pset1<Packet>(RealScalar(0));
}
}
for (Index k = 0; k < depth; ++k) {
const RealScalar* a = rA + k * 2 * pw;
const RealScalar* b = rB + k * 2 * cw;
Packet are[NPack], aim[NPack];
for (int p = 0; p < NPack; ++p) {
are[p] = ploadu<Packet>(a + p * PS);
aim[p] = ploadu<Packet>(a + pw + p * PS);
}
sme_neon_col_loop<0, NCol>::template cplx<ConjLhs, ConjRhs, Cols, Packet, NPack>(
acc_re, acc_im, are, aim, Cols::load(b), Cols::load(b + cw));
}
const RealScalar ar = numext::real(alpha), ai = numext::imag(alpha);
const Packet valpha_re = pset1<Packet>(ar), valpha_im = pset1<Packet>(ai);
for (int c = 0; c < NCol; ++c) {
for (int p = 0; p < NPack; ++p) {
RealScalar* pc = reinterpret_cast<RealScalar*>(C + Index(p * PS) * rs + Index(c) * cs);
if (rs == 1) {
// Unit row stride: the column is PS interleaved (re, im) pairs.
Packet cre, cim;
sme_neon_ld2(pc, cre, cim);
cre = pnmadd(valpha_im, acc_im[p][c], pmadd(valpha_re, acc_re[p][c], cre));
cim = pmadd(valpha_im, acc_re[p][c], pmadd(valpha_re, acc_im[p][c], cim));
sme_neon_st2(pc, cre, cim);
} else {
RealScalar re[PS], im[PS];
pstoreu(re, acc_re[p][c]);
pstoreu(im, acc_im[p][c]);
for (int l = 0; l < PS; ++l) {
RealScalar* pl = pc + Index(l) * 2 * rs;
pl[0] += ar * re[l] - ai * im[l];
pl[1] += ar * im[l] + ai * re[l];
}
}
}
}
}
};
template <typename RealScalar, typename Index, bool ConjLhs, bool ConjRhs, int NCol>
struct sme_neon_ctile<RealScalar, Index, ConjLhs, ConjRhs, 0, NCol> {
using Scalar = std::complex<RealScalar>;
static EIGEN_ALWAYS_INLINE void run(Scalar* C, Index rs, Index cs, const RealScalar* rA, const RealScalar* rB,
Index depth, Scalar alpha, Index pw, Index cw) {
EIGEN_UNUSED_VARIABLE(rs);
constexpr int KU = (NCol >= 4) ? 1 : 4 / NCol;
RealScalar acc_re[KU][NCol], acc_im[KU][NCol];
for (int u = 0; u < KU; ++u)
for (int c = 0; c < NCol; ++c) acc_re[u][c] = acc_im[u][c] = RealScalar(0);
for (Index k = 0; k < depth; ++k) {
const int u = int(k % KU);
const RealScalar* a = rA + k * 2 * pw;
const RealScalar* b = rB + k * 2 * cw;
const RealScalar are = a[0], aim = ConjLhs ? -a[pw] : a[pw];
for (int c = 0; c < NCol; ++c) {
const RealScalar bre = b[c], bim = ConjRhs ? -b[cw + c] : b[cw + c];
acc_re[u][c] += are * bre - aim * bim;
acc_im[u][c] += are * bim + aim * bre;
}
}
for (int u = 1; u < KU; ++u)
for (int c = 0; c < NCol; ++c) {
acc_re[0][c] += acc_re[u][c];
acc_im[0][c] += acc_im[u][c];
}
const RealScalar ar = numext::real(alpha), ai = numext::imag(alpha);
for (int c = 0; c < NCol; ++c) {
RealScalar* pc = reinterpret_cast<RealScalar*>(C + Index(c) * cs);
pc[0] += ar * acc_re[0][c] - ai * acc_im[0][c];
pc[1] += ar * acc_im[0][c] + ai * acc_re[0][c];
}
}
};
// Row loop of one column group: 3, 2 and 1 packets of rows, then scalar rows.
template <bool ConjLhs, bool ConjRhs, int NCol, typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_neon_column_group(Scalar* C, Index rs, Index cs, const Scalar* blA, const Scalar* blB,
Index depth, Scalar alpha, Index pw, Index cw) {
constexpr Index PS = Index(packet_traits<Scalar>::size);
Index r = 0;
for (; r + 3 * PS <= pw; r += 3 * PS)
sme_neon_tile<Scalar, Index, 3, NCol>::run(C + r * rs, rs, cs, blA + r, blB, depth, alpha, pw, cw);
for (; r + 2 * PS <= pw; r += 2 * PS)
sme_neon_tile<Scalar, Index, 2, NCol>::run(C + r * rs, rs, cs, blA + r, blB, depth, alpha, pw, cw);
for (; r + PS <= pw; r += PS)
sme_neon_tile<Scalar, Index, 1, NCol>::run(C + r * rs, rs, cs, blA + r, blB, depth, alpha, pw, cw);
for (; r < pw; ++r)
sme_neon_tile<Scalar, Index, 0, NCol>::run(C + r * rs, rs, cs, blA + r, blB, depth, alpha, pw, cw);
}
template <bool ConjLhs, bool ConjRhs, int NCol, typename RealScalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_neon_column_group(std::complex<RealScalar>* C, Index rs, Index cs, const RealScalar* rA,
const RealScalar* rB, Index depth, std::complex<RealScalar> alpha,
Index pw, Index cw) {
// Two accumulators per packet, so the ladder stops at two packets of rows.
constexpr Index PS = Index(packet_traits<RealScalar>::size);
Index r = 0;
for (; r + 2 * PS <= pw; r += 2 * PS)
sme_neon_ctile<RealScalar, Index, ConjLhs, ConjRhs, 2, NCol>::run(C + r * rs, rs, cs, rA + r, rB, depth, alpha, pw,
cw);
for (; r + PS <= pw; r += PS)
sme_neon_ctile<RealScalar, Index, ConjLhs, ConjRhs, 1, NCol>::run(C + r * rs, rs, cs, rA + r, rB, depth, alpha, pw,
cw);
for (; r < pw; ++r)
sme_neon_ctile<RealScalar, Index, ConjLhs, ConjRhs, 0, NCol>::run(C + r * rs, rs, cs, rA + r, rB, depth, alpha, pw,
cw);
}
// One pw x cw block: columns in groups of eight, four, two, then single columns.
template <bool ConjLhs, bool ConjRhs, typename Scalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_neon_block(Scalar* C, Index rs, Index cs, const Scalar* blA, const Scalar* blB,
Index depth, Scalar alpha, Index pw, Index cw) {
Index c = 0;
for (; c + 8 <= cw; c += 8)
sme_neon_column_group<ConjLhs, ConjRhs, 8>(C + c * cs, rs, cs, blA, blB + c, depth, alpha, pw, cw);
for (; c + 4 <= cw; c += 4)
sme_neon_column_group<ConjLhs, ConjRhs, 4>(C + c * cs, rs, cs, blA, blB + c, depth, alpha, pw, cw);
for (; c + 2 <= cw; c += 2)
sme_neon_column_group<ConjLhs, ConjRhs, 2>(C + c * cs, rs, cs, blA, blB + c, depth, alpha, pw, cw);
for (; c < cw; ++c)
sme_neon_column_group<ConjLhs, ConjRhs, 1>(C + c * cs, rs, cs, blA, blB + c, depth, alpha, pw, cw);
}
template <bool ConjLhs, bool ConjRhs, typename RealScalar, typename Index>
EIGEN_ALWAYS_INLINE void sme_neon_block(std::complex<RealScalar>* C, Index rs, Index cs,
const std::complex<RealScalar>* blA, const std::complex<RealScalar>* blB,
Index depth, std::complex<RealScalar> alpha, Index pw, Index cw) {
const RealScalar* rA = reinterpret_cast<const RealScalar*>(blA);
const RealScalar* rB = reinterpret_cast<const RealScalar*>(blB);
Index c = 0;
for (; c + 4 <= cw; c += 4)
sme_neon_column_group<ConjLhs, ConjRhs, 4>(C + c * cs, rs, cs, rA, rB + c, depth, alpha, pw, cw);
for (; c + 2 <= cw; c += 2)
sme_neon_column_group<ConjLhs, ConjRhs, 2>(C + c * cs, rs, cs, rA, rB + c, depth, alpha, pw, cw);
for (; c < cw; ++c) sme_neon_column_group<ConjLhs, ConjRhs, 1>(C + c * cs, rs, cs, rA, rB + c, depth, alpha, pw, cw);
}
// Same panel walk as sme_gebp_impl, outside any streaming region.
template <typename Scalar, bool ConjLhs, bool ConjRhs, typename Index>
EIGEN_ALWAYS_INLINE void sme_gebp_neon(Scalar* C, Index C_stride_row, Index C_stride_col, const Scalar* blockA,
const Scalar* blockB, Index rows, Index depth, Index cols, Scalar alpha,
Index strideA, Index strideB, Index offsetA, Index offsetB) {
constexpr Index MR = Index(sme_block<Scalar>::mr);
constexpr Index NR = Index(sme_block<Scalar>::nr);
for (Index j = 0; j < cols; j += NR) {
const Index cw = numext::mini(cols - j, NR);
const Scalar* blB = blockB + j * strideB + offsetB * cw;
for (Index i = 0; i < rows; i += MR) {
const Index pw = numext::mini(rows - i, MR);
const Scalar* blA = blockA + i * strideA + offsetA * pw;
sme_neon_block<ConjLhs, ConjRhs>(C + i * C_stride_row + j * C_stride_col, C_stride_row, C_stride_col, blA, blB,
depth, alpha, pw, cw);
}
}
}
template <typename Scalar, typename Index, typename DataMapper, int mr, int nr, bool ConjugateLhs, bool ConjugateRhs>
struct sme_gebp_kernel {
using ResScalar = Scalar;
EIGEN_DONT_INLINE void operator()(const DataMapper& res, const Scalar* blockA, const Scalar* blockB, Index rows,
Index depth, Index cols, ResScalar alpha, Index strideA = -1, Index strideB = -1,
Index offsetA = 0, Index offsetB = 0) {
// Real scalars never reach the kernel conjugated (conj_helper folds it into
// the identity long before), so the real path stays free of the flags.
static_assert(NumTraits<Scalar>::IsComplex || (!ConjugateLhs && !ConjugateRhs),
"the SME kernel does not support conjugation of real scalars");
static_assert(mr == sme_block<Scalar>::mr && nr == sme_block<Scalar>::nr,
"the SME kernel expects packed panels of the SME block width");
if (strideA == -1) strideA = depth;
if (strideB == -1) strideB = depth;
if (rows <= 0 || cols <= 0 || depth <= 0) return;
Scalar* C_base = const_cast<Scalar*>(&res(0, 0));
const Index C_stride_row = &res(1, 0) - &res(0, 0);
const Index C_stride_col = &res(0, 1) - &res(0, 0);
if (sme_kernel_with_neon<Scalar>(rows, cols, depth, strideA, strideB)) {
sme_gebp_neon<Scalar, ConjugateLhs, ConjugateRhs>(C_base, C_stride_row, C_stride_col, blockA, blockB, rows, depth,
cols, alpha, strideA, strideB, offsetA, offsetB);
return;
}
sme_fpsr_guard fpsr;
sme_gebp_dispatch<Scalar, ConjugateLhs, ConjugateRhs>::run(C_base, C_stride_row, C_stride_col, blockA, blockB, rows,
depth, cols, alpha, strideA, strideB, offsetA, offsetB);
}
// The LHS block read from its ColMajor source (see sme_direct_lhs_ok); only
// instantiated for real scalars.
EIGEN_DONT_INLINE void run_direct_lhs(const DataMapper& res, const Scalar* lhs, Index lhsStride, const Scalar* blockB,
Index rows, Index depth, Index cols, ResScalar alpha, Index strideB = -1,
Index offsetB = 0) {
if (strideB == -1) strideB = depth;
if (rows <= 0 || cols <= 0 || depth <= 0) return;
Scalar* C_base = const_cast<Scalar*>(&res(0, 0));
const Index C_stride_row = &res(1, 0) - &res(0, 0);
const Index C_stride_col = &res(0, 1) - &res(0, 0);
sme_fpsr_guard fpsr;
sme_gebp_impl_direct_lhs<Scalar, Index>(C_base, C_stride_row, C_stride_col, lhs, lhsStride, blockB, rows, depth,
cols, alpha, strideB, offsetB);
}
};
#define EIGEN_SME_DECLARE_GEBP_KERNEL(SCALAR) \
template <typename Index, typename DataMapper, int mr, int nr, bool ConjugateLhs, bool ConjugateRhs> \
struct gebp_kernel<SCALAR, SCALAR, Index, DataMapper, mr, nr, ConjugateLhs, ConjugateRhs> \
: sme_gebp_kernel<SCALAR, Index, DataMapper, mr, nr, ConjugateLhs, ConjugateRhs> {};
EIGEN_SME_DECLARE_GEBP_KERNEL(float)
EIGEN_SME_DECLARE_GEBP_KERNEL(std::complex<float>)
#ifdef EIGEN_VECTORIZE_SME_F64F64
EIGEN_SME_DECLARE_GEBP_KERNEL(double)
EIGEN_SME_DECLARE_GEBP_KERNEL(std::complex<double>)
#endif
#undef EIGEN_SME_DECLARE_GEBP_KERNEL
// sme_has_gebp_kernel (products/GeneralBlockPanelKernel.h) drives the cache
// blocking and the GEMM loop order, and is declared before this header. A pair
// listed there but not specialized here would be packed and blocked for SME and
// then handed to the generic kernel.
static_assert(sme_has_gebp_kernel<float, float>::value, "the SME float kernel is not advertised to the GEMM driver");
static_assert(sme_has_gebp_kernel<std::complex<float>, std::complex<float>>::value,
"the SME complex<float> kernel is not advertised to the GEMM driver");
#ifdef EIGEN_VECTORIZE_SME_F64F64
static_assert(sme_has_gebp_kernel<double, double>::value, "the SME double kernel is not advertised to the GEMM driver");
static_assert(sme_has_gebp_kernel<std::complex<double>, std::complex<double>>::value,
"the SME complex<double> kernel is not advertised to the GEMM driver");
#else
static_assert(!sme_has_gebp_kernel<double, double>::value,
"double is advertised to the GEMM driver without FEAT_SME_F64F64 to implement it");
static_assert(!sme_has_gebp_kernel<std::complex<double>, std::complex<double>>::value,
"complex<double> is advertised to the GEMM driver without FEAT_SME_F64F64 to implement it");
#endif
// ---------------------------------------------------------------------------
// Selfadjoint (SYMM) packers.
//
// product_selfadjoint_matrix packs the selfadjoint operand (stored as one
// triangle) through symm_pack_lhs/symm_pack_rhs, which materialize the full
// matrix as they pack. The generic SYMM packers emit packet-width sub-panels
// for the generic gebp_kernel, whereas the SME kernel expects uniform
// mr/nr-wide depth-major panels. These packers perform the same
// triangle mirroring in the SME layout.
//
// The packer receives the operand in an orientation where row >= col is the
// stored triangle. It reads that half directly and mirrors the other half:
// full(row,col) = (row >= col) ? m(row,col) : conj(m(col,row))
// and a selfadjoint view defines the diagonal's imaginary part as zero, so
// full(k,k) is real(m(k,k)). Both reduce to the identity on real scalars.
//
// Regions wholly below or above the diagonal use the normal dense copy or
// transpose packers. Only the width-wide part of a panel crossed by the
// diagonal needs special handling: each depth row is split between the stored
// triangle and its mirrored half.
//
// For a panel at offset j (entries j+c, c in [0,w)) and global row k2+k, the
// three depth regions are:
// transposed k in [0, j-k2) : k2+k < j+c for all c -> m(j+c, k2+k)
// straddle k in [j-k2, j+w-k2) : diagonal crosses -> per-k split
// direct k in [j+w-k2, depth): k2+k > j+c for all c -> m(k2+k, j+c)
//
// The RHS packs full(k2+k, j+c) and uses this mapping directly, so its mirrored
// half is the transposed region. The LHS packs full(j+r, k), which is the
// conjugate of full(k, j+r), so it reuses the same mapping with k2 == 0
// relative to its diagonal-anchored base pointer but conjugates the opposite
// regions -- the direct one and the straddle band's head. IsLhs selects which.
// ---------------------------------------------------------------------------
// Streaming packer shared by the LHS (k2 == 0) and RHS symm specializations.
// ColM selects the ColMajor selfadjoint operand.
// Depth-region boundaries for the panel at outer offset `j`, all clamped to
// [0, depth]: the diagonal splits it into a transposed head [0, t_end), a
// straddle band [t_end, s_end) and a direct tail [s_end, depth).
template <typename Index>
static EIGEN_ALWAYS_INLINE void sme_symm_panel_regions(Index j, int w, Index depth, Index k2, Index& t_end,
Index& s_end) __arm_streaming_compatible {
const Index raw_t = j - k2, raw_s = j + Index(w) - k2;
t_end = raw_t <= 0 ? Index(0) : sme_min(raw_t, depth);
s_end = raw_s <= 0 ? Index(0) : sme_min(raw_s, depth);
}
// The two dense regions of every panel, which are ordinary copies or ZA
// transposes of the stored triangle. ColM selects the ColMajor operand.
template <typename Scalar, int StorageOrder, bool IsLhs, typename Index>
EIGEN_DONT_INLINE __arm_locally_streaming __arm_new("za") void sme_symm_pack_dense_regions(
Scalar* block, const Scalar* EIGEN_RESTRICT base, Index stride, Index depth, Index outer, Index k2) {
constexpr int PACK = IsLhs ? sme_block<Scalar>::mr : sme_block<Scalar>::nr;
constexpr bool ColM = (StorageOrder == ColMajor);
// The transposed region is the RHS's mirrored half and the direct one the
// LHS's, so exactly one of the two is conjugated (see above).
constexpr bool ConjTransposed = !IsLhs;
constexpr bool ConjDirect = IsLhs;
for (Index j = 0; j < outer; j += PACK) {
const int w = static_cast<int>(sme_min(outer - j, Index(PACK)));
Scalar* dst = block + j * depth; // depth-major panel of width w
Index t_end, s_end;
sme_symm_panel_regions(j, w, depth, k2, t_end, s_end);
// Transposed region: full(k2+k, j+c) = m(j+c, k2+k).
if (t_end > 0) {
EIGEN_IF_CONSTEXPR (ColM) {
sve_copy_panel_range<ConjTransposed>(dst, base + j + k2 * stride, stride, Index(0), t_end, w);
} else {
sme_transpose_pack_range<ConjTransposed>(dst, base + j * stride + k2, stride, Index(0), t_end, w);
}
}
// Direct region: full(k2+k, j+c) = m(k2+k, j+c).
if (s_end < depth) {
EIGEN_IF_CONSTEXPR (ColM) {
sme_transpose_pack_range<ConjDirect>(dst, base + k2 + j * stride, stride, s_end, depth, w);
} else {
sve_copy_panel_range<ConjDirect>(dst, base + k2 * stride + j, stride, s_end, depth, w);
}
}
}
}
// The diagonal band of every panel: the diagonal crosses at c* = (k2+k) - j
// (in [0, w) throughout the band), so each depth step splits into a direct head
// (c < c*: m(k2+k, j+c)) and a mirrored tail (c >= c*: m(j+c, k2+k); at c == c*
// both name the diagonal element).
//
// Kept out of the streaming region above for the reason tail_transpose_pack gives,
// at the cost of a second pass over the panels: it is scalar floating-point,
// and fusing it made the float SYMM packers 2-11x slower.
template <typename Scalar, int StorageOrder, bool IsLhs, typename Index>
EIGEN_DONT_INLINE void sme_symm_pack_straddle(Scalar* block, const Scalar* EIGEN_RESTRICT base, Index stride,
Index depth, Index outer, Index k2) {
constexpr int PACK = IsLhs ? sme_block<Scalar>::mr : sme_block<Scalar>::nr;
constexpr bool ColM = (StorageOrder == ColMajor);
// The band's tail is the RHS's mirrored half and its head the LHS's, exactly
// as the dense regions above.
constexpr bool ConjHead = IsLhs;
constexpr bool ConjTail = !IsLhs;
for (Index j = 0; j < outer; j += PACK) {
const int w = static_cast<int>(numext::mini(outer - j, Index(PACK)));
Scalar* dst = block + j * depth;
Index t_end, s_end;
sme_symm_panel_regions(j, w, depth, k2, t_end, s_end);
for (Index k = t_end; k < s_end; ++k) {
const Index row = k2 + k;
const int cs = static_cast<int>(row - j);
Scalar* dst_row = dst + k * w;
const Index wi = Index(w);
EIGEN_IF_CONSTEXPR (ColM) {
const Scalar* head = base + row + j * stride; // m(row, j+c): stride-strided
for (int c = 0; c < cs; ++c, head += stride) sme_pack_store<ConjHead>(dst_row, wi, Index(c), *head);
const Scalar* tail = base + j + row * stride; // m(j+c, row): contiguous
int c = cs;
// A selfadjoint view defines the diagonal's imaginary part as zero;
// for a real scalar that is already true, so the peel folds away.
EIGEN_IF_CONSTEXPR (NumTraits<Scalar>::IsComplex) {
sme_pack_store<false>(dst_row, wi, Index(cs), Scalar(numext::real(tail[cs])));
++c;
}
for (; c < w; ++c) sme_pack_store<ConjTail>(dst_row, wi, Index(c), tail[c]);
} else {
const Scalar* head = base + row * stride + j; // m(row, j+c): contiguous
for (int c = 0; c < cs; ++c) sme_pack_store<ConjHead>(dst_row, wi, Index(c), head[c]);
const Scalar* tail = base + (j + Index(cs)) * stride + row; // m(j+c, row): stride-strided
int c = cs;
EIGEN_IF_CONSTEXPR (NumTraits<Scalar>::IsComplex) {
sme_pack_store<false>(dst_row, wi, Index(cs), Scalar(numext::real(*tail)));
tail += stride;
++c;
}
for (; c < w; ++c, tail += stride) sme_pack_store<ConjTail>(dst_row, wi, Index(c), *tail);
}
}
}
}
// Packer shared by the LHS (k2 == 0) and RHS symm specializations.
template <typename Scalar, int StorageOrder, bool IsLhs, typename Index>
EIGEN_DONT_INLINE void sme_symm_pack_panels(Scalar* block, const Scalar* EIGEN_RESTRICT base, Index stride, Index depth,
Index outer, Index k2) {
{
sme_fpsr_guard fpsr;
sme_symm_pack_dense_regions<Scalar, StorageOrder, IsLhs, Index>(block, base, stride, depth, outer, k2);
}
sme_symm_pack_straddle<Scalar, StorageOrder, IsLhs, Index>(block, base, stride, depth, outer, k2);
}
// symm_pack_lhs/rhs SME specializations: emit the uniform mr/nr panels
// sme_gebp_impl reads. Pack1/nr pinned exactly as gemm_pack_lhs/rhs above.
template <typename Scalar, int StorageOrder, typename Index>
struct sme_symm_pack_lhs {
// Note: generic symm_pack_lhs's "cols" is the depth extent, and the LHS
// block is diagonal-anchored (base = &lhs(k2,k2)), so its depth offset is 0.
EIGEN_DONT_INLINE void operator()(Scalar* blockA, const Scalar* lhs_, Index lhsStride, Index cols, Index rows) const {
sme_symm_pack_panels<Scalar, StorageOrder, true, Index>(blockA, lhs_, lhsStride, cols, rows, Index(0));
}
};
template <typename Scalar, int StorageOrder, typename Index>
struct sme_symm_pack_rhs {
// Note: generic symm_pack_rhs's "rows" is the depth extent (end_k = k2 + rows), not a row count.
EIGEN_DONT_INLINE void operator()(Scalar* blockB, const Scalar* rhs_, Index rhsStride, Index rows, Index cols,
Index k2) const {
sme_symm_pack_panels<Scalar, StorageOrder, false, Index>(blockB, rhs_, rhsStride, rows, cols, k2);
}
};
#define EIGEN_SME_DECLARE_SYMM_PACKERS(SCALAR, MR, NR) \
template <typename Index, int Pack2_dummy, int StorageOrder> \
struct symm_pack_lhs<SCALAR, Index, MR, Pack2_dummy, StorageOrder> \
: sme_symm_pack_lhs<SCALAR, StorageOrder, Index> {}; \
\
template <typename Index, int StorageOrder> \
struct symm_pack_rhs<SCALAR, Index, NR, StorageOrder> : sme_symm_pack_rhs<SCALAR, StorageOrder, Index> {};
EIGEN_SME_DECLARE_SYMM_PACKERS(float, kSmeMr, kSmeNr)
EIGEN_SME_DECLARE_SYMM_PACKERS(std::complex<float>, kSmeMrC, kSmeNrC)
#ifdef EIGEN_VECTORIZE_SME_F64F64
EIGEN_SME_DECLARE_SYMM_PACKERS(double, kSmeMrD, kSmeNrD)
EIGEN_SME_DECLARE_SYMM_PACKERS(std::complex<double>, kSmeMrCD, kSmeNrCD)
#endif
#undef EIGEN_SME_DECLARE_SYMM_PACKERS
// Tiny results (at most 2 * PS x 8): a NEON outer-product kernel with the whole result in registers, called before
// any blocking or packing. Both the coeff-based product (one dependent FMA chain per result) and the packed paths
// are several times slower there.
template <typename Scalar>
struct sme_tiny_neon;
template <>
struct sme_tiny_neon<float> {
using V = float32x4_t;
static constexpr int PS = 4;
static EIGEN_ALWAYS_INLINE V ld(const float* p) { return vld1q_f32(p); }
static EIGEN_ALWAYS_INLINE V zero() { return vdupq_n_f32(0.f); }
// The first n (< PS) scalars at p, zeros after them.
static EIGEN_ALWAYS_INLINE V ld_part(const float* p, int n) {
if (n <= 0) return zero();
if (n == 1) return vld1q_lane_f32(p, zero(), 0);
const V v = vcombine_f32(vld1_f32(p), vdup_n_f32(0.f));
return n == 2 ? v : vld1q_lane_f32(p + 2, v, 2);
}
// A full load permuted by byte indices; indices of 16 or more give zeros.
static EIGEN_ALWAYS_INLINE V ld_tbl(const float* p, uint8x16_t idx) {
return vreinterpretq_f32_u8(vqtbl1q_u8(vreinterpretq_u8_f32(vld1q_f32(p)), idx));
}
template <int L>
static EIGEN_ALWAYS_INLINE V fma_lane(V c, V a, V b) {
return vfmaq_laneq_f32(c, a, b, L);
}
static EIGEN_ALWAYS_INLINE V fma_n(V c, V a, float b) { return vfmaq_n_f32(c, a, b); }
static EIGEN_ALWAYS_INLINE void st(float* p, V v) { vst1q_f32(p, v); }
static EIGEN_ALWAYS_INLINE V add(V a, V b) { return vaddq_f32(a, b); }
// Rows in, columns out.
static EIGEN_ALWAYS_INLINE void transpose(V (&r)[PS]) {
const float64x2_t t0 = vreinterpretq_f64_f32(vtrn1q_f32(r[0], r[1]));
const float64x2_t t1 = vreinterpretq_f64_f32(vtrn2q_f32(r[0], r[1]));
const float64x2_t t2 = vreinterpretq_f64_f32(vtrn1q_f32(r[2], r[3]));
const float64x2_t t3 = vreinterpretq_f64_f32(vtrn2q_f32(r[2], r[3]));
r[0] = vreinterpretq_f32_f64(vtrn1q_f64(t0, t2));
r[1] = vreinterpretq_f32_f64(vtrn1q_f64(t1, t3));
r[2] = vreinterpretq_f32_f64(vtrn2q_f64(t0, t2));
r[3] = vreinterpretq_f32_f64(vtrn2q_f64(t1, t3));
}
};
template <>
struct sme_tiny_neon<double> {
using V = float64x2_t;
static constexpr int PS = 2;
static EIGEN_ALWAYS_INLINE V ld(const double* p) { return vld1q_f64(p); }
static EIGEN_ALWAYS_INLINE V zero() { return vdupq_n_f64(0.); }
static EIGEN_ALWAYS_INLINE V ld_part(const double* p, int n) { return n > 0 ? vld1q_lane_f64(p, zero(), 0) : zero(); }
static EIGEN_ALWAYS_INLINE V ld_tbl(const double* p, uint8x16_t idx) {
return vreinterpretq_f64_u8(vqtbl1q_u8(vreinterpretq_u8_f64(vld1q_f64(p)), idx));
}
template <int L>
static EIGEN_ALWAYS_INLINE V fma_lane(V c, V a, V b) {
return vfmaq_laneq_f64(c, a, b, L);
}
static EIGEN_ALWAYS_INLINE V fma_n(V c, V a, double b) { return vfmaq_n_f64(c, a, b); }
static EIGEN_ALWAYS_INLINE void st(double* p, V v) { vst1q_f64(p, v); }
static EIGEN_ALWAYS_INLINE V add(V a, V b) { return vaddq_f64(a, b); }
static EIGEN_ALWAYS_INLINE void transpose(V (&r)[PS]) {
const V t0 = vtrn1q_f64(r[0], r[1]);
r[1] = vtrn2q_f64(r[0], r[1]);
r[0] = t0;
}
};
// One depth chunk of FMAs; the lane index must be a constant, hence the recursion over it. Depth step u of a chunk
// accumulates into set u % S: four FMA pipes of latency four need about 16 independent chains.
template <typename T, int RV, int NC, int S>
struct sme_tiny_step {
using V = typename T::V;
static constexpr int PS = T::PS;
// ColMajor B: b[j] holds depth steps k .. k + PS - 1 of column j; lane U is step k + U.
template <int U>
static EIGEN_ALWAYS_INLINE void col(V (&acc)[S][NC][RV], const V (&a)[PS][RV], const V (&b)[NC],
std::integral_constant<int, U>) {
EIGEN_UNROLL_LOOP
for (int j = 0; j < NC; ++j) {
EIGEN_UNROLL_LOOP
for (int r = 0; r < RV; ++r) acc[U % S][j][r] = T::template fma_lane<U>(acc[U % S][j][r], a[U][r], b[j]);
}
col(acc, a, b, std::integral_constant<int, U + 1>());
}
static EIGEN_ALWAYS_INLINE void col(V (&)[S][NC][RV], const V (&)[PS][RV], const V (&)[NC],
std::integral_constant<int, PS>) {}
// RowMajor B: b[c] holds columns c * PS .. of one depth step.
template <int J>
static EIGEN_ALWAYS_INLINE void row(V (&acc)[NC][RV], const V (&a)[RV], const V (&b)[NC / PS],
std::integral_constant<int, J>) {
EIGEN_UNROLL_LOOP
for (int r = 0; r < RV; ++r) acc[J][r] = T::template fma_lane<J % PS>(acc[J][r], a[r], b[J / PS]);
row(acc, a, b, std::integral_constant<int, J + 1>());
}
static EIGEN_ALWAYS_INLINE void row(V (&)[NC][RV], const V (&)[RV], const V (&)[NC / PS],
std::integral_constant<int, NC>) {}
};
// C (m x n, m <= RV * PS, n <= NC) += alpha * A * B, reading only A's m rows and B's n columns: rows and columns
// past the result repeat the last valid one or load as zeros, and their sums are dropped.
template <typename Scalar, int RV, int NC, int LhsOrder, int RhsOrder, typename Index>
EIGEN_DONT_INLINE void sme_tiny_gemm_kernel(Index m, Index n, Index depth, const Scalar* A, Index lda, const Scalar* B,
Index ldb, Scalar* C, Index incr, Index ldc, Scalar alpha) {
using T = sme_tiny_neon<Scalar>;
using V = typename T::V;
constexpr int PS = T::PS, MR = RV * PS, S = 16 / (RV * NC) < PS ? 16 / (RV * NC) : PS;
using Step = sme_tiny_step<T, RV, NC, S>;
V acc[S][NC][RV];
EIGEN_UNROLL_LOOP
for (int t = 0; t < S; ++t) {
EIGEN_UNROLL_LOOP
for (int j = 0; j < NC; ++j) {
EIGEN_UNROLL_LOOP
for (int r = 0; r < RV; ++r) acc[t][j][r] = T::zero();
}
}
const Scalar* a_row[MR];
for (int i = 0; i < MR; ++i) a_row[i] = A + numext::mini(Index(i), m - 1) * lda;
const Scalar* b_col[NC];
for (int j = 0; j < NC; ++j) b_col[j] = B + numext::mini(Index(j), n - 1) * ldb;
// A column (B row) vector r holds cnt valid scalars. Once m (n) >= PS, each is one full load ending at its last
// valid scalar, shifted down by a byte table with zeros after; below that, a partial load.
int a_cnt[RV], b_cnt[NC / PS];
Index a_off[RV], b_off[NC / PS];
uint8x16_t a_idx[RV], b_idx[NC / PS];
const auto shift_table = [](int cnt) {
EIGEN_ALIGN16 uint8_t t[16];
for (int b = 0; b < 16; ++b)
t[b] = b < cnt * int(sizeof(Scalar)) ? uint8_t(b + (PS - cnt) * int(sizeof(Scalar))) : 0xff;
return vld1q_u8(t);
};
for (int r = 0; r < RV; ++r) {
a_cnt[r] = int(numext::mini(Index(PS), m - r * PS));
a_off[r] = r * PS + a_cnt[r] - PS;
a_idx[r] = shift_table(a_cnt[r]);
}
for (int c = 0; c < NC / PS; ++c) {
b_cnt[c] = int(numext::maxi(Index(0), numext::mini(Index(PS), n - c * PS)));
b_off[c] = numext::maxi(Index(0), Index(c * PS + b_cnt[c] - PS));
b_idx[c] = shift_table(b_cnt[c]);
}
const bool a_full = m >= PS, b_full = n >= PS;
const Index kmain = (depth / PS) * PS;
for (Index k = 0; k < kmain; k += PS) {
V a[PS][RV];
EIGEN_IF_CONSTEXPR (LhsOrder == ColMajor) {
EIGEN_UNROLL_LOOP
for (int u = 0; u < PS; ++u) {
EIGEN_UNROLL_LOOP
for (int r = 0; r < RV; ++r)
a[u][r] = a_cnt[r] == PS ? T::ld(A + (k + u) * lda + r * PS)
: a_full ? T::ld_tbl(A + (k + u) * lda + a_off[r], a_idx[r])
: T::ld_part(A + (k + u) * lda + r * PS, a_cnt[r]);
}
} else {
EIGEN_UNROLL_LOOP
for (int r = 0; r < RV; ++r) {
V blk[PS];
EIGEN_UNROLL_LOOP
for (int q = 0; q < PS; ++q) blk[q] = T::ld(a_row[r * PS + q] + k);
T::transpose(blk);
EIGEN_UNROLL_LOOP
for (int u = 0; u < PS; ++u) a[u][r] = blk[u];
}
}
EIGEN_IF_CONSTEXPR (RhsOrder == ColMajor) {
V b[NC];
EIGEN_UNROLL_LOOP
for (int j = 0; j < NC; ++j) b[j] = T::ld(b_col[j] + k);
Step::col(acc, a, b, std::integral_constant<int, 0>());
} else {
EIGEN_UNROLL_LOOP
for (int u = 0; u < PS; ++u) {
V b[NC / PS];
EIGEN_UNROLL_LOOP
for (int c = 0; c < NC / PS; ++c)
b[c] = b_cnt[c] == PS ? T::ld(B + (k + u) * ldb + c * PS)
: b_cnt[c] == 0 ? T::zero()
: b_full ? T::ld_tbl(B + (k + u) * ldb + b_off[c], b_idx[c])
: T::ld_part(B + (k + u) * ldb + c * PS, b_cnt[c]);
Step::row(acc[u % S], a[u], b, std::integral_constant<int, 0>());
}
}
}
for (Index k = kmain; k < depth; ++k) {
EIGEN_ALIGN16 Scalar at[MR];
for (int i = 0; i < MR; ++i)
at[i] = LhsOrder == ColMajor ? A[k * lda + numext::mini(Index(i), m - 1)] : a_row[i][k];
V a[RV];
EIGEN_UNROLL_LOOP
for (int r = 0; r < RV; ++r) a[r] = T::ld(at + r * PS);
EIGEN_UNROLL_LOOP
for (int j = 0; j < NC; ++j) {
const Scalar b = RhsOrder == ColMajor ? b_col[j][k] : B[k * ldb + numext::mini(Index(j), n - 1)];
EIGEN_UNROLL_LOOP
for (int r = 0; r < RV; ++r) acc[0][j][r] = T::fma_n(acc[0][j][r], a[r], b);
}
}
EIGEN_UNROLL_LOOP
for (int t = 1; t < S; ++t) {
EIGEN_UNROLL_LOOP
for (int j = 0; j < NC; ++j) {
EIGEN_UNROLL_LOOP
for (int r = 0; r < RV; ++r) acc[0][j][r] = T::add(acc[0][j][r], acc[t][j][r]);
}
}
EIGEN_ALIGN16 Scalar out[NC][MR];
EIGEN_UNROLL_LOOP
for (int j = 0; j < NC; ++j) {
EIGEN_UNROLL_LOOP
for (int r = 0; r < RV; ++r) T::st(&out[j][r * PS], acc[0][j][r]);
}
for (Index j = 0; j < n; ++j)
for (Index i = 0; i < m; ++i) C[i * incr + j * ldc] += alpha * out[j][i];
}
template <typename Scalar, int LhsOrder, int RhsOrder, typename Index>
bool sme_tiny_gemm(Index rows, Index cols, Index depth, const Scalar* lhs, Index lhsStride, const Scalar* rhs,
Index rhsStride, Scalar* res, Index resIncr, Index resStride, Scalar alpha) {
constexpr int PS = sme_tiny_neon<Scalar>::PS;
if (!sme_tiny_gemm_wins<Scalar>(rows, cols, depth)) return false;
const bool one_vec = rows <= PS, four_cols = cols <= 4;
if (one_vec && four_cols)
sme_tiny_gemm_kernel<Scalar, 1, 4, LhsOrder, RhsOrder>(rows, cols, depth, lhs, lhsStride, rhs, rhsStride, res,
resIncr, resStride, alpha);
else if (one_vec)
sme_tiny_gemm_kernel<Scalar, 1, 8, LhsOrder, RhsOrder>(rows, cols, depth, lhs, lhsStride, rhs, rhsStride, res,
resIncr, resStride, alpha);
else if (four_cols)
sme_tiny_gemm_kernel<Scalar, 2, 4, LhsOrder, RhsOrder>(rows, cols, depth, lhs, lhsStride, rhs, rhsStride, res,
resIncr, resStride, alpha);
else
sme_tiny_gemm_kernel<Scalar, 2, 8, LhsOrder, RhsOrder>(rows, cols, depth, lhs, lhsStride, rhs, rhsStride, res,
resIncr, resStride, alpha);
return true;
}
} // namespace internal
} // namespace Eigen
#endif // EIGEN_SME_GENERALBLOCKPANELKERNEL_H