| // 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 per-element-width half of the ACLE surface: everything the kernel needs |
| // that is selected by the element type alone rather than by an argument. |
| // Operations that can be overloaded on their arguments are free functions |
| // below, and the ones taking a ZA tile number take it as a template parameter |
| // because the underlying instructions encode it as an immediate. |
| // |
| // `whilelt` takes int64_t in both specializations rather than the caller's own |
| // integer type: svwhilelt_b* is overloaded on the four fixed-width types only, |
| // so an argument of a distinct type of the same width -- `Index` wherever |
| // int64_t is `long long` -- matches none of them exactly. Signed, because |
| // every bound the packers pass is a non-negative Index. |
| template <typename Scalar> |
| struct sme_traits; |
| |
| // ZA tile count at a given element width: 4 ZA.S tiles, 8 ZA.D tiles. This is |
| // architectural rather than feature-dependent, so it stays outside sme_traits, |
| // which has no double specialization without FEAT_SME_F64F64. |
| template <typename RealScalar> |
| struct sme_tile_count; |
| template <> |
| struct sme_tile_count<float> { |
| static constexpr int value = 4; |
| }; |
| template <> |
| struct sme_tile_count<double> { |
| static constexpr int value = 8; |
| }; |
| |
| template <> |
| struct sme_traits<float> { |
| using Vec = svfloat32_t; |
| using Vec2 = svfloat32x2_t; |
| using Vec4 = svfloat32x4_t; |
| static EIGEN_ALWAYS_INLINE int svl() __arm_streaming_compatible { return static_cast<int>(svcntsw()); } |
| static EIGEN_ALWAYS_INLINE svbool_t whilelt(int64_t begin, int64_t end) __arm_streaming { |
| return svwhilelt_b32(begin, end); |
| } |
| static EIGEN_ALWAYS_INLINE svbool_t ptrue() __arm_streaming { return svptrue_b32(); } |
| static EIGEN_ALWAYS_INLINE svcount_t ptrue_c() __arm_streaming { return svptrue_c32(); } |
| static EIGEN_ALWAYS_INLINE Vec dup(float x) __arm_streaming { return svdup_f32(x); } |
| }; |
| |
| #ifdef EIGEN_VECTORIZE_SME_F64F64 |
| template <> |
| struct sme_traits<double> { |
| using Vec = svfloat64_t; |
| using Vec2 = svfloat64x2_t; |
| using Vec4 = svfloat64x4_t; |
| static EIGEN_ALWAYS_INLINE int svl() __arm_streaming_compatible { return static_cast<int>(svcntsd()); } |
| static EIGEN_ALWAYS_INLINE svbool_t whilelt(int64_t begin, int64_t end) __arm_streaming { |
| return svwhilelt_b64(begin, end); |
| } |
| static EIGEN_ALWAYS_INLINE svbool_t ptrue() __arm_streaming { return svptrue_b64(); } |
| static EIGEN_ALWAYS_INLINE svcount_t ptrue_c() __arm_streaming { return svptrue_c64(); } |
| static EIGEN_ALWAYS_INLINE Vec dup(double x) __arm_streaming { return svdup_f64(x); } |
| }; |
| #endif |
| |
| // Contiguous predicated load/store, fused multiply-add and multiply. |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_ld1(svbool_t pg, const float* p) __arm_streaming { return svld1_f32(pg, p); } |
| static EIGEN_ALWAYS_INLINE void sme_st1(svbool_t pg, float* p, svfloat32_t v) __arm_streaming { svst1_f32(pg, p, v); } |
| static EIGEN_ALWAYS_INLINE svfloat32x2_t sme_ld1_x2(svcount_t pn, const float* p) __arm_streaming { |
| return svld1_f32_x2(pn, p); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat32x4_t sme_ld1_x4(svcount_t pn, const float* p) __arm_streaming { |
| return svld1_f32_x4(pn, p); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_mla(svbool_t pg, svfloat32_t acc, svfloat32_t a, |
| svfloat32_t b) __arm_streaming { |
| return svmla_f32_x(pg, acc, a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_mul(svbool_t pg, svfloat32_t a, svfloat32_t b) __arm_streaming { |
| return svmul_f32_x(pg, a, b); |
| } |
| template <int Lane> |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_get(svfloat32x2_t v) __arm_streaming { |
| return svget2_f32(v, Lane); |
| } |
| template <int Lane> |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_get(svfloat32x4_t v) __arm_streaming { |
| return svget4_f32(v, Lane); |
| } |
| |
| // ZA tile access. The tile number is an instruction immediate, hence a template |
| // parameter; the slice number is a register operand and stays a value. |
| template <int Tile> |
| static EIGEN_ALWAYS_INLINE void sme_ld1_hor_za(uint32_t slice, svbool_t pg, |
| const float* p) __arm_streaming __arm_inout("za") { |
| svld1_hor_za32(Tile, slice, pg, p); |
| } |
| template <int Tile> |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_read_hor_za(svfloat32_t zero, svbool_t pg, |
| uint32_t slice) __arm_streaming __arm_inout("za") { |
| return svread_hor_za32_f32_m(zero, pg, Tile, slice); |
| } |
| template <int Tile> |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_read_ver_za(svfloat32_t zero, svbool_t pg, |
| uint32_t slice) __arm_streaming __arm_inout("za") { |
| return svread_ver_za32_f32_m(zero, pg, Tile, slice); |
| } |
| template <int Tile> |
| static EIGEN_ALWAYS_INLINE void sme_mopa(svbool_t pm, svbool_t pn, svfloat32_t a, |
| svfloat32_t b) __arm_streaming __arm_inout("za") { |
| svmopa_za32_f32_m(Tile, pm, pn, a, b); |
| } |
| template <int Tile> |
| static EIGEN_ALWAYS_INLINE void sme_mops(svbool_t pm, svbool_t pn, svfloat32_t a, |
| svfloat32_t b) __arm_streaming __arm_inout("za") { |
| svmops_za32_f32_m(Tile, pm, pn, a, b); |
| } |
| |
| // Fused multiply-subtract, negation, and the de-/interleaving permutes the |
| // complex kernel needs on top of the real one. |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_mls(svbool_t pg, svfloat32_t acc, svfloat32_t a, |
| svfloat32_t b) __arm_streaming { |
| return svmls_f32_x(pg, acc, a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_add(svbool_t pg, svfloat32_t a, svfloat32_t b) __arm_streaming { |
| return svadd_f32_x(pg, a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_neg(svbool_t pg, svfloat32_t v) __arm_streaming { |
| return svneg_f32_x(pg, v); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_uzp1(svfloat32_t a, svfloat32_t b) __arm_streaming { |
| return svuzp1_f32(a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_uzp2(svfloat32_t a, svfloat32_t b) __arm_streaming { |
| return svuzp2_f32(a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_zip1(svfloat32_t a, svfloat32_t b) __arm_streaming { |
| return svzip1_f32(a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat32_t sme_zip2(svfloat32_t a, svfloat32_t b) __arm_streaming { |
| return svzip2_f32(a, b); |
| } |
| |
| #ifdef EIGEN_VECTORIZE_SME_F64F64 |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_ld1(svbool_t pg, const double* p) __arm_streaming { |
| return svld1_f64(pg, p); |
| } |
| static EIGEN_ALWAYS_INLINE void sme_st1(svbool_t pg, double* p, svfloat64_t v) __arm_streaming { svst1_f64(pg, p, v); } |
| static EIGEN_ALWAYS_INLINE svfloat64x2_t sme_ld1_x2(svcount_t pn, const double* p) __arm_streaming { |
| return svld1_f64_x2(pn, p); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat64x4_t sme_ld1_x4(svcount_t pn, const double* p) __arm_streaming { |
| return svld1_f64_x4(pn, p); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_mla(svbool_t pg, svfloat64_t acc, svfloat64_t a, |
| svfloat64_t b) __arm_streaming { |
| return svmla_f64_x(pg, acc, a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_mul(svbool_t pg, svfloat64_t a, svfloat64_t b) __arm_streaming { |
| return svmul_f64_x(pg, a, b); |
| } |
| template <int Lane> |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_get(svfloat64x2_t v) __arm_streaming { |
| return svget2_f64(v, Lane); |
| } |
| template <int Lane> |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_get(svfloat64x4_t v) __arm_streaming { |
| return svget4_f64(v, Lane); |
| } |
| |
| template <int Tile> |
| static EIGEN_ALWAYS_INLINE void sme_ld1_hor_za(uint32_t slice, svbool_t pg, |
| const double* p) __arm_streaming __arm_inout("za") { |
| svld1_hor_za64(Tile, slice, pg, p); |
| } |
| template <int Tile> |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_read_hor_za(svfloat64_t zero, svbool_t pg, |
| uint32_t slice) __arm_streaming __arm_inout("za") { |
| return svread_hor_za64_f64_m(zero, pg, Tile, slice); |
| } |
| template <int Tile> |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_read_ver_za(svfloat64_t zero, svbool_t pg, |
| uint32_t slice) __arm_streaming __arm_inout("za") { |
| return svread_ver_za64_f64_m(zero, pg, Tile, slice); |
| } |
| template <int Tile> |
| static EIGEN_ALWAYS_INLINE void sme_mopa(svbool_t pm, svbool_t pn, svfloat64_t a, |
| svfloat64_t b) __arm_streaming __arm_inout("za") { |
| svmopa_za64_f64_m(Tile, pm, pn, a, b); |
| } |
| template <int Tile> |
| static EIGEN_ALWAYS_INLINE void sme_mops(svbool_t pm, svbool_t pn, svfloat64_t a, |
| svfloat64_t b) __arm_streaming __arm_inout("za") { |
| svmops_za64_f64_m(Tile, pm, pn, a, b); |
| } |
| |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_mls(svbool_t pg, svfloat64_t acc, svfloat64_t a, |
| svfloat64_t b) __arm_streaming { |
| return svmls_f64_x(pg, acc, a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_add(svbool_t pg, svfloat64_t a, svfloat64_t b) __arm_streaming { |
| return svadd_f64_x(pg, a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_neg(svbool_t pg, svfloat64_t v) __arm_streaming { |
| return svneg_f64_x(pg, v); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_uzp1(svfloat64_t a, svfloat64_t b) __arm_streaming { |
| return svuzp1_f64(a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_uzp2(svfloat64_t a, svfloat64_t b) __arm_streaming { |
| return svuzp2_f64(a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_zip1(svfloat64_t a, svfloat64_t b) __arm_streaming { |
| return svzip1_f64(a, b); |
| } |
| static EIGEN_ALWAYS_INLINE svfloat64_t sme_zip2(svfloat64_t a, svfloat64_t b) __arm_streaming { |
| return svzip2_f64(a, b); |
| } |
| #endif // EIGEN_VECTORIZE_SME_F64F64 |
| |
| // Outer-product accumulate with a compile-time sign: the complex kernel's four |
| // real products differ only in whether they add or subtract into the tile. |
| template <int Tile, bool Subtract, typename Vec> |
| static EIGEN_ALWAYS_INLINE void sme_mopa_signed(svbool_t pm, svbool_t pn, Vec a, |
| Vec b) __arm_streaming __arm_inout("za") { |
| EIGEN_IF_CONSTEXPR (Subtract) { |
| sme_mops<Tile>(pm, pn, a, b); |
| } else { |
| sme_mopa<Tile>(pm, pn, a, b); |
| } |
| } |
| |
| // 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 |
| |
| // min() usable from streaming functions (numext::mini lacks the |
| // __arm_streaming_compatible attribute). |
| template <typename T> |
| static EIGEN_ALWAYS_INLINE T sme_min(T a, T b) __arm_streaming_compatible { |
| return a < b ? a : b; |
| } |
| |
| // Offset a pointer by n elements without forming the pointer value: an access |
| // whose predicate is empty makes no memory reference, but computing an address |
| // more than one past the end of the object is undefined regardless, so the |
| // second-vector accesses below reach their address through uintptr_t. |
| template <typename T> |
| static EIGEN_ALWAYS_INLINE T* sme_offset(T* p, Index n) __arm_streaming_compatible { |
| return reinterpret_cast<T*>(uintptr_t(p) + ptrdiff_t(n) * sizeof(T)); |
| } |
| |
| // --------------------------------------------------------------------------- |
| // 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 { |
| const int svl = sme_traits<Scalar>::svl(); |
| for (int off = 0; off < width; off += svl) { |
| const svbool_t pred = sme_traits<Scalar>::whilelt(off, width); |
| for (Index k = k0; k < k1; ++k) { |
| sme_st1(pred, &dst[k * width + off], sme_ld1(pred, &src[k * src_stride + off])); |
| } |
| } |
| } |
| |
| // 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_traits<RealScalar>; |
| using Vec = typename Traits::Vec; |
| RealScalar* EIGEN_RESTRICT rdst = reinterpret_cast<RealScalar*>(dst); |
| const RealScalar* EIGEN_RESTRICT rsrc = reinterpret_cast<const RealScalar*>(src); |
| const int svl = Traits::svl(); |
| 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 = sme_ld1(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 = sme_ld1(pg_hi, sme_offset(p, svl)); |
| Vec im = sme_uzp2(v_lo, v_hi); |
| EIGEN_IF_CONSTEXPR (Conjugate) { |
| im = sme_neg(pg_w, im); |
| } |
| sme_st1(pg_w, &rdst[k * step + Index(off)], sme_uzp1(v_lo, v_hi)); |
| sme_st1(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); |
| } |
| |
| // 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_traits<RealScalar>; |
| using Vec = typename Traits::Vec; |
| const Vec zero = Traits::dup(RealScalar(0)); |
| const svbool_t pg_all = Traits::ptrue(); |
| const int svl = Traits::svl(); |
| |
| for (Index k = k0; 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 = sme_neg(pg_all, v0); |
| v1 = sme_neg(pg_all, v1); |
| } |
| } |
| sme_st1(pg_all, &dst[(k + c) * width + r0], v0); |
| sme_st1(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 = sme_neg(pg_r, v0); |
| } |
| sme_st1(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); |
| } |
| |
| // --------------------------------------------------------------------------- |
| // 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)); |
| } |
| } |
| } |
| } |
| |
| // 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. |
| template <bool UsePacketPath, typename Scalar, typename Index, typename DataMapper, typename DirectFn, |
| typename FallbackFn> |
| EIGEN_ALWAYS_INLINE void sme_dispatch_pack(DirectFn direct, 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; |
| direct(block, src, m.stride(), depth, n, stride, offset); |
| } else { |
| fallback(block, m, depth, n, stride, offset, UsePacketPath); |
| } |
| } |
| template <bool UsePacketPath, typename Scalar, typename Index, typename DataMapper, typename DirectFn, |
| typename FallbackFn> |
| EIGEN_ALWAYS_INLINE void sme_dispatch_pack(DirectFn, 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 { |
| __arm_locally_streaming 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; |
| |
| // Full panels of width MR, streamed in svl-wide predicated chunks. |
| 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; |
| 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>(&pack_direct, &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 { |
| __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 = (rows / MR) * 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]). This branch runs at most |
| // once per pack_lhs call with < MR rows and would need a partial-ZA-tile |
| // dance to vectorise; total copies are < MR * depth per call, which is |
| // noise vs the main packer's workload, so scalar is the simple choice -- |
| // taken outside the streaming region above (see tail_transpose_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_transpose_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>(&pack_direct, &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 { |
| __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 = (cols / NR) * 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]). Same reasoning as the LHS |
| // RowMajor packer's row tail: runs at most once per call, < NR cols, not |
| // worth the partial-ZA-tile handling, and taken outside the streaming |
| // region above (see tail_transpose_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_transpose_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>(&pack_direct, &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 { |
| __arm_locally_streaming 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>(&pack_direct, &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_traits<Scalar>; |
| using Vec = typename Traits::Vec; |
| 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 = Traits::dup(Scalar(0)); |
| const Vec valpha = Traits::dup(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 = sme_ld1(pg_m, p0); |
| Vec c1 = sme_ld1(pg_m, p1); |
| sme_st1(pg_m, p0, sme_mla(pg_m, c0, sme_read_ver_za<TileId>(vzero, pg_m, (uint32_t)ci), valpha)); |
| sme_st1(pg_m, p1, sme_mla(pg_m, c1, sme_read_ver_za<TileId>(vzero, pg_m, (uint32_t)(ci + 1)), valpha)); |
| } |
| if (ci < cw) { |
| Scalar* pC = C + row_start + (col_start + ci) * C_stride_col; |
| Vec vc = sme_ld1(pg_m, pC); |
| sme_st1(pg_m, pC, sme_mla(pg_m, vc, sme_read_ver_za<TileId>(vzero, pg_m, (uint32_t)ci), valpha)); |
| } |
| } 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 = sme_ld1(pg_n, p0); |
| Vec c1 = sme_ld1(pg_n, p1); |
| sme_st1(pg_n, p0, sme_mla(pg_n, c0, sme_read_hor_za<TileId>(vzero, pg_n, (uint32_t)ri), valpha)); |
| sme_st1(pg_n, p1, sme_mla(pg_n, c1, sme_read_hor_za<TileId>(vzero, pg_n, (uint32_t)(ri + 1)), valpha)); |
| } |
| if (ri < pw) { |
| Scalar* pC = C + (row_start + ri) * C_stride_row + col_start; |
| Vec vc = sme_ld1(pg_n, pC); |
| sme_st1(pg_n, pC, sme_mla(pg_n, vc, sme_read_hor_za<TileId>(vzero, pg_n, (uint32_t)ri), valpha)); |
| } |
| } 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 = sme_mul(pg_n, vres, valpha); |
| sme_st1(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). |
| *****************************************************************************/ |
| |
| 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_traits<Scalar>::svl(); |
| 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_traits<Scalar>::Vec a_lo, typename sme_traits<Scalar>::Vec a_hi, typename sme_traits<Scalar>::Vec b_lo, |
| typename sme_traits<Scalar>::Vec b_hi) __arm_streaming __arm_inout("za") { |
| const svbool_t all = sme_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_scaled_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. |
| * |
| * 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_traits<RealScalar>::Vec valpha_re, |
| typename sme_traits<RealScalar>::Vec valpha_im, |
| typename sme_traits<RealScalar>::Vec vzero, uint32_t slice, |
| typename sme_traits<RealScalar>::Vec& lo, |
| typename sme_traits<RealScalar>::Vec& hi) __arm_streaming __arm_inout("za") { |
| using Vec = typename sme_traits<RealScalar>::Vec; |
| 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 = sme_mls(pg, sme_mul(pg, re, valpha_re), im, valpha_im); |
| const Vec out_im = sme_mla(pg, sme_mul(pg, im, valpha_re), re, valpha_im); |
| lo = sme_zip1(out_re, out_im); |
| hi = sme_zip2(out_re, out_im); |
| } else { |
| lo = sme_zip1(re, im); |
| hi = sme_zip2(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_traits<RealScalar>::Vec valpha_re, typename sme_traits<RealScalar>::Vec valpha_im, |
| typename sme_traits<RealScalar>::Vec vzero) __arm_streaming __arm_inout("za") { |
| using Traits = sme_traits<RealScalar>; |
| using Vec = typename Traits::Vec; |
| const int svl = Traits::svl(); |
| 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); |
| sme_st1(pl0, p, sme_add(pl0, sme_ld1(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)); |
| sme_st1(pl1, phi, sme_add(pl1, sme_ld1(pl1, phi), hi)); |
| } |
| } |
| |
| template <typename RealScalar, int TileRe, int TileIm, bool Vertical, typename Index> |
| EIGEN_ALWAYS_INLINE void sme_accumulate_pair( |
| bool scale_by_alpha, RealScalar* EIGEN_RESTRICT p, Index step, int slices, int lanes, svbool_t pg, |
| typename sme_traits<RealScalar>::Vec valpha_re, typename sme_traits<RealScalar>::Vec valpha_im, |
| typename sme_traits<RealScalar>::Vec vzero) __arm_streaming __arm_inout("za") { |
| 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, 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) __arm_streaming __arm_inout("za") { |
| using Scalar = std::complex<RealScalar>; |
| using Traits = sme_traits<RealScalar>; |
| using Vec = typename Traits::Vec; |
| const int svl = Traits::svl(); |
| const svbool_t pg_m = Traits::whilelt(0, pw); |
| const svbool_t pg_n = Traits::whilelt(0, cw); |
| const Vec vzero = Traits::dup(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 = Traits::dup(alpha_parts[0]); |
| const Vec valpha_im = Traits::dup(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. |
| 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>(scale, p, Index(2) * C_stride_col, cw, 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>(scale, p, Index(2) * C_stride_row, pw, 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); |
| sme_st1(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. |
| sme_st1(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_traits<RealScalar>::Vec; |
| 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 <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) __arm_streaming __arm_inout("za") { |
| sme_store_za_pair<RealScalar, kTileRe, kTileIm>(dst, C_stride_row, C_stride_col, alpha, row_start, pw, col_start, |
| cw); |
| } |
| }; |
| |
| 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_traits<typename NumTraits<Scalar>::Real>::Vec; |
| static EIGEN_ALWAYS_INLINE void accumulate(svbool_t, svbool_t, Vec, Vec, Vec, Vec) __arm_streaming __arm_inout("za") { |
| } |
| template <typename Index> |
| static EIGEN_ALWAYS_INLINE void store(Scalar*, Index, Index, Scalar, Index, int, Index, |
| int) __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) __arm_streaming __arm_inout("za") { |
| // Conjugation is the identity on real scalars, so this overload ignores it. |
| using Traits = sme_traits<Scalar>; |
| using Vec = typename Traits::Vec; |
| const int svl = Traits::svl(); |
| |
| 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; |
| for (; k < depth_4; k += 4) { |
| typename Traits::Vec4 va_01 = sme_ld1_x4(pn, &blA[k * pw]); |
| typename Traits::Vec4 vb_01 = sme_ld1_x4(pn, &blB[k * cw]); |
| |
| // d0 |
| outer_product_2x2<Scalar>(sme_get<0>(va_01), sme_get<1>(va_01), sme_get<0>(vb_01), sme_get<1>(vb_01)); |
| // d1 |
| outer_product_2x2<Scalar>(sme_get<2>(va_01), sme_get<3>(va_01), sme_get<2>(vb_01), sme_get<3>(vb_01)); |
| |
| typename Traits::Vec4 va_23 = sme_ld1_x4(pn, &blA[(k + 2) * pw]); |
| typename Traits::Vec4 vb_23 = sme_ld1_x4(pn, &blB[(k + 2) * cw]); |
| |
| // d2 |
| outer_product_2x2<Scalar>(sme_get<0>(va_23), sme_get<1>(va_23), sme_get<0>(vb_23), sme_get<1>(vb_23)); |
| // d3 |
| outer_product_2x2<Scalar>(sme_get<2>(va_23), sme_get<3>(va_23), sme_get<2>(vb_23), sme_get<3>(vb_23)); |
| } |
| // Depth tail: one x2 load per side per step. |
| for (; k < depth; ++k) { |
| typename Traits::Vec2 va = sme_ld1_x2(pn, &blA[k * pw]); |
| typename Traits::Vec2 vb = sme_ld1_x2(pn, &blB[k * cw]); |
| outer_product_2x2<Scalar>(sme_get<0>(va), sme_get<1>(va), sme_get<0>(vb), sme_get<1>(vb)); |
| } |
| } else { |
| for (Index k = 0; k < depth; ++k) { |
| Vec a_lo = sme_ld1(pg_rlo, &blA[k * pw + rt]); |
| Vec b_lo = sme_ld1(pg_clo, &blB[k * cw + ct]); |
| |
| Vec a_hi = sme_ld1(pg_rhi, sme_offset(blA, k * pw + rt + svl)); |
| Vec b_hi = sme_ld1(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, 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) __arm_streaming __arm_inout("za") { |
| const int svl = sme_traits<typename NumTraits<Scalar>::Real>::svl(); |
| sme_complex_cell<Scalar, 0, 0, ConjLhs, ConjRhs>::store(C, C_stride_row, C_stride_col, alpha, row_start, rlo, |
| col_start, clo); |
| if (chi > 0) { |
| sme_complex_cell<Scalar, 0, 1, ConjLhs, ConjRhs>::store(C, C_stride_row, C_stride_col, alpha, row_start, rlo, |
| col_start + svl, chi); |
| } |
| if (rhi > 0) { |
| sme_complex_cell<Scalar, 1, 0, ConjLhs, ConjRhs>::store(C, C_stride_row, C_stride_col, alpha, row_start + svl, rhi, |
| col_start, clo); |
| if (chi > 0) { |
| sme_complex_cell<Scalar, 1, 1, ConjLhs, ConjRhs>::store(C, C_stride_row, C_stride_col, alpha, row_start + svl, |
| rhi, col_start + svl, chi); |
| } |
| } |
| } |
| |
| /***************************************************************************** |
| * 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, 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) __arm_streaming __arm_inout("za") { |
| using Scalar = std::complex<RealScalar>; |
| using Traits = sme_traits<RealScalar>; |
| using Vec = typename Traits::Vec; |
| const int svl = Traits::svl(); |
| 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 = sme_ld1(pg_r0, pa); |
| const Vec a0_im = sme_ld1(pg_r0, pa + pw); |
| const Vec b0_re = sme_ld1(pg_c0, pb); |
| const Vec b0_im = sme_ld1(pg_c0, pb + cw); |
| const Vec a1_re = sme_ld1(pg_r1, sme_offset(pa, Index(svl))); |
| const Vec a1_im = sme_ld1(pg_r1, sme_offset(pa, Index(pw) + Index(svl))); |
| const Vec b1_re = sme_ld1(pg_c1, sme_offset(pb, Index(svl))); |
| const Vec b1_im = sme_ld1(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>(C, C_stride_row, C_stride_col, alpha, row_start + rt, r0, r1, |
| col_start + ct, c0, c1); |
| } |
| } |
| } |
| |
| template <typename Scalar, bool ConjLhs, bool ConjRhs, 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 sme_process; partial |
| // blocks are tiled and predicated inside the generic path. |
| 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; |
| |
| for (Index i = 0; i < rows; i += MR) { |
| const int pw = static_cast<int>(sme_min(rows - i, Index(MR))); |
| const Scalar* blA = blockA + i * strideA + offsetA * pw; |
| sme_process<ConjLhs, ConjRhs>(C, C_stride_row, C_stride_col, blA, blB, depth, alpha, i, pw, j, 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); |
| |
| sme_gebp_impl<Scalar, ConjugateLhs, ConjugateRhs>(C_base, C_stride_row, C_stride_col, blockA, blockB, rows, depth, |
| cols, alpha, strideA, strideB, offsetA, 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_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 |
| |
| } // namespace internal |
| } // namespace Eigen |
| |
| #endif // EIGEN_SME_GENERALBLOCKPANELKERNEL_H |