ARM NEON
NEON backend for VectorF32 on AArch64: one float32x4_t per vector (Len * Packing <= 4), with a vectorised sin/cos port of the SSE helpers. VectorF16 gets a native float16x8_t path when the target has ARMv8.2-FP16 arithmetic. Without it VectorF16 aliases VectorF32, which converts fp16 <-> fp32 on Load/Store. The scalar fallback gains the batched Dot that MatrixRowMajor relies on. Test tiers added for armv8-a and armv8.2-a+fp16. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
751a896d10
commit
0c55e76873
6 changed files with 825 additions and 17 deletions
|
|
@ -8,6 +8,9 @@ module;
|
|||
#ifdef __wasm_simd128__
|
||||
#include <wasm_simd128.h>
|
||||
#endif
|
||||
#if defined(__aarch64__) && defined(__ARM_NEON)
|
||||
#include <arm_neon.h>
|
||||
#endif
|
||||
#ifdef __riscv_vector
|
||||
#include <riscv_vector.h>
|
||||
// Compile-time VLEN selection. RVV is VLA at the ISA level, but storage in
|
||||
|
|
@ -37,9 +40,11 @@ import std;
|
|||
// VectorF16 exists as a real struct when _Float16 is available AND we are not
|
||||
// on x86_64 without AVX512FP16 (that path aliases VectorF16 to VectorF32 in
|
||||
// Crafter.Math:Basic for performance). The same alias kicks in on RISC-V until
|
||||
// a Zvfh path lands. Each translation unit that needs this distinction
|
||||
// redefines the same condition since macros do not cross module boundaries.
|
||||
#if defined(__FLT16_MAX__) && (!defined(__x86_64) || defined(__AVX512FP16__)) && !defined(__riscv_vector)
|
||||
// a Zvfh path lands, and on AArch64 NEON without ARMv8.2-FP16 arithmetic,
|
||||
// where VectorF32 converts fp16 <-> fp32 on Load/Store. Each translation unit
|
||||
// that needs this distinction redefines the same condition since macros do not
|
||||
// cross module boundaries.
|
||||
#if defined(__FLT16_MAX__) && (!defined(__x86_64) || defined(__AVX512FP16__)) && !defined(__riscv_vector) && !(defined(__aarch64__) && defined(__ARM_NEON) && !defined(__ARM_FEATURE_FP16_VECTOR_ARITHMETIC))
|
||||
namespace Crafter {
|
||||
export template <std::uint8_t Len, std::uint8_t Packing>
|
||||
struct VectorF16;
|
||||
|
|
@ -52,7 +57,7 @@ namespace Crafter {
|
|||
|
||||
template <std::uint8_t Len, std::uint8_t Packing, typename T>
|
||||
struct VectorBase {
|
||||
#if defined(__FLT16_MAX__) && (!defined(__x86_64) || defined(__AVX512FP16__)) && !defined(__riscv_vector)
|
||||
#if defined(__FLT16_MAX__) && (!defined(__x86_64) || defined(__AVX512FP16__)) && !defined(__riscv_vector) && !(defined(__aarch64__) && defined(__ARM_NEON) && !defined(__ARM_FEATURE_FP16_VECTOR_ARITHMETIC))
|
||||
template <std::uint8_t L, std::uint8_t P>
|
||||
friend struct VectorF16;
|
||||
#endif
|
||||
|
|
@ -89,6 +94,11 @@ namespace Crafter {
|
|||
>;
|
||||
#elif defined(__wasm_simd128__)
|
||||
using VectorType = v128_t;
|
||||
#elif defined(__aarch64__) && defined(__ARM_NEON)
|
||||
// NEON registers are 128 bits wide, so every instantiation is one
|
||||
// register: four floats, or eight halves for the native VectorF16
|
||||
// that exists when the target has ARMv8.2-FP16 arithmetic.
|
||||
using VectorType = std::conditional_t<std::is_same_v<T, float>, float32x4_t, float16x8_t>;
|
||||
#elif defined(__riscv_vector)
|
||||
// RVV tier mirrors the x86 selector: pick the widest register the
|
||||
// toolchain guarantees, then size each instantiation down to the
|
||||
|
|
@ -118,6 +128,10 @@ namespace Crafter {
|
|||
// WASM SIMD only has 128-bit vectors; cap at 16 bytes so the entire
|
||||
// VectorType always fits in a single v128_t.
|
||||
static constexpr std::uint8_t Max = 16;
|
||||
#elif defined(__aarch64__) && defined(__ARM_NEON)
|
||||
// NEON only has 128-bit vectors; cap at 16 bytes so the entire
|
||||
// VectorType always fits in a single register.
|
||||
static constexpr std::uint8_t Max = 16;
|
||||
#elif defined(__riscv_vector)
|
||||
// RVV tier selected at compile time from the guaranteed VLEN. ZVL128B
|
||||
// is the RVA23 baseline; ZVL256B / ZVL512B unlock wider registers
|
||||
|
|
@ -698,5 +712,77 @@ namespace Crafter {
|
|||
out_sin = _mm512_xor_ps(out_sin, x_sign);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__aarch64__) && defined(__ARM_NEON)
|
||||
// --- 128-bit (NEON) helpers, same algorithm as the SSE ones above ---
|
||||
// Reduce |x| into [-pi/4, pi/4], return reduced value and quadrant
|
||||
static constexpr void range_reduce_f32x4(float32x4_t ax, float32x4_t& r, float32x4_t& r2, int32x4_t& q) {
|
||||
float32x4_t fq = vrndnq_f32(vmulq_f32(ax, vdupq_n_f32(two_over_pi)));
|
||||
q = vcvtq_s32_f32(fq);
|
||||
// ax - fq * pi/2 as an fmla with the constant negated, not as
|
||||
// vfmsq_f32: that one is a wrapper function in clang's arm_neon.h,
|
||||
// and calling it from two module units (here and :VectorF32) fails
|
||||
// some importers with "definition with same mangled name as
|
||||
// another definition".
|
||||
r = vfmaq_f32(ax, fq, vdupq_n_f32(-pi_over_2_hi));
|
||||
r = vfmaq_f32(r, fq, vdupq_n_f32(-pi_over_2_lo));
|
||||
r2 = vmulq_f32(r, r);
|
||||
}
|
||||
|
||||
// vfmaq_f32(a, b, c) is a + b*c, so each Horner step reads
|
||||
// "next coefficient + accumulator * r2".
|
||||
static constexpr void sincos_poly_f32x4(float32x4_t r, float32x4_t r2, float32x4_t& cos_r, float32x4_t& sin_r) {
|
||||
cos_r = vfmaq_f32(vdupq_n_f32(c8), vdupq_n_f32(c10), r2);
|
||||
cos_r = vfmaq_f32(vdupq_n_f32(c6), cos_r, r2);
|
||||
cos_r = vfmaq_f32(vdupq_n_f32(c4), cos_r, r2);
|
||||
cos_r = vfmaq_f32(vdupq_n_f32(c2), cos_r, r2);
|
||||
cos_r = vfmaq_f32(vdupq_n_f32(c0), cos_r, r2);
|
||||
|
||||
sin_r = vfmaq_f32(vdupq_n_f32(s7), vdupq_n_f32(s9), r2);
|
||||
sin_r = vfmaq_f32(vdupq_n_f32(s5), sin_r, r2);
|
||||
sin_r = vfmaq_f32(vdupq_n_f32(s3), sin_r, r2);
|
||||
sin_r = vfmaq_f32(vdupq_n_f32(s1), sin_r, r2);
|
||||
sin_r = vfmaq_f32(vdupq_n_f32(1.0f), sin_r, r2);
|
||||
sin_r = vmulq_f32(sin_r, r);
|
||||
}
|
||||
|
||||
static constexpr void sincos_f32x4(float32x4_t x, float32x4_t& out_sin, float32x4_t& out_cos) {
|
||||
uint32x4_t x_sign = vandq_u32(vreinterpretq_u32_f32(x), vdupq_n_u32(0x80000000u));
|
||||
float32x4_t ax = vabsq_f32(x);
|
||||
|
||||
float32x4_t r, r2; int32x4_t q;
|
||||
range_reduce_f32x4(ax, r, r2, q);
|
||||
|
||||
float32x4_t cos_r, sin_r;
|
||||
sincos_poly_f32x4(r, r2, cos_r, sin_r);
|
||||
|
||||
uint32x4_t is_odd = vtstq_s32(q, vdupq_n_s32(1));
|
||||
|
||||
// cos: swap on odd, negate if (q+1)&2
|
||||
out_cos = vbslq_f32(is_odd, sin_r, cos_r);
|
||||
int32x4_t cos_neg = vandq_s32(vaddq_s32(q, vdupq_n_s32(1)), vdupq_n_s32(2));
|
||||
out_cos = vreinterpretq_f32_s32(veorq_s32(vreinterpretq_s32_f32(out_cos), vshlq_n_s32(cos_neg, 30)));
|
||||
|
||||
// sin: swap on odd, negate if q&2, apply input sign
|
||||
out_sin = vbslq_f32(is_odd, cos_r, sin_r);
|
||||
int32x4_t sin_neg = vandq_s32(q, vdupq_n_s32(2));
|
||||
out_sin = vreinterpretq_f32_s32(veorq_s32(vreinterpretq_s32_f32(out_sin), vshlq_n_s32(sin_neg, 30)));
|
||||
out_sin = vreinterpretq_f32_u32(veorq_u32(vreinterpretq_u32_f32(out_sin), x_sign));
|
||||
}
|
||||
|
||||
// The single-result variants share sincos_f32x4; the half that is not
|
||||
// returned is dead code the compiler drops after inlining.
|
||||
static constexpr float32x4_t cos_f32x4(float32x4_t x) {
|
||||
float32x4_t s, c;
|
||||
sincos_f32x4(x, s, c);
|
||||
return c;
|
||||
}
|
||||
|
||||
static constexpr float32x4_t sin_f32x4(float32x4_t x) {
|
||||
float32x4_t s, c;
|
||||
sincos_f32x4(x, s, c);
|
||||
return s;
|
||||
}
|
||||
#endif
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue