From e1a1b2a7e62e105fa66cf72f98198e3a297e2a2c Mon Sep 17 00:00:00 2001 From: Axel Huebl Date: Thu, 26 Feb 2026 11:18:43 -0800 Subject: [PATCH 1/6] SIMD: Add `where` and tenary operator To write more portable code, add a scalar fallback for `std::simd::where` for non-SIMD code. Expose both as `amrex::simd::stdx`. Add Doxygen string to existing scalar `amrex::simd::std::any_of` fallback. Add a new implementation for a portable tenary operator, inspired by Kokkos https://kokkos.org/kokkos-core-wiki/ProgrammingGuide/SIMD.html#ternary-operator as `amrex::simd::conditional`. Demonstrate usage in doc strings and new test cases. --- Src/Base/AMReX_SIMD.H | 89 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/Src/Base/AMReX_SIMD.H b/Src/Base/AMReX_SIMD.H index 9c89cdee556..821ef2e723c 100644 --- a/Src/Base/AMReX_SIMD.H +++ b/Src/Base/AMReX_SIMD.H @@ -31,8 +31,57 @@ namespace amrex::simd #else // fallback implementations for functions that are commonly used in portable code paths + /** True if the boolean value is true (scalar identity fallback for simd any_of) + * + * Example: + * ```cpp + * // Works for both simd_mask and scalar bool: + * auto mask = a > b; + * if (amrex::simd::stdx::any_of(mask)) { ... } + * ``` + * + * @see https://en.cppreference.com/w/cpp/experimental/simd/any_of + */ AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE bool any_of (bool const v) { return v; } + + /// \cond DOXYGEN_IGNORE + namespace detail { + template + struct where_expression { + bool mask; + T& value; + + AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE + void operator= (T const& new_val) + { + if (mask) { value = new_val; } + } + }; + } + /// \endcond + + /** Masked assignment expression (scalar fallback for simd where) + * + * Returns an expression object whose assignment operator conditionally + * updates value only when mask is true. + * + * Example: + * ```cpp + * // Works for both simd and scalar T: + * auto mask = b > T(0); + * T result = T(0); + * amrex::simd::stdx::where(mask, result) = a / b; + * ``` + * + * @see https://en.cppreference.com/w/cpp/experimental/simd/where + */ + template + AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE + detail::where_expression where (bool const mask, T& value) + { + return {mask, value}; + } #endif } @@ -165,6 +214,46 @@ namespace amrex::simd return val_arr[n]; } + /** Vectorized ternary operator: condition(mask, true_val, false_val) + * + * Selects elements from true_val where mask is true and from false_val where + * mask is false. Analogous to (mask ? true_val : false_val) for scalars. + * + * Note: both true_val and false_val are eagerly evaluated (function arguments). + * To guard against operations like division by zero, sanitize inputs before + * the operation rather than relying on conditional selection. + * + * Example: + * ```cpp + * template + * T compute (T const& a, T const& b) + * { + * auto safe_b = amrex::simd::condition(b != T(0), b, T(1)); + * return amrex::simd::condition(b != T(0), a / safe_b, T(0)); + * } + * ``` + */ +#ifdef AMREX_USE_SIMD + template + AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE + stdx::simd condition ( + typename stdx::simd::mask_type const& mask, + stdx::simd const& true_val, + stdx::simd const& false_val) + { + stdx::simd result = false_val; + stdx::where(mask, result) = true_val; + return result; + } +#else + template + AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE + T condition (bool const mask, T const& true_val, T const& false_val) + { + return mask ? true_val : false_val; + } +#endif + /** Load 1D contiguous data from array pointers * * On GPU and CPU w/o SIMD, this dereferences a 1D array element at the From 39aeb6f71f688e9314e7fba02e66b8a4873343f8 Mon Sep 17 00:00:00 2001 From: Axel Huebl Date: Thu, 26 Feb 2026 11:20:53 -0800 Subject: [PATCH 2/6] SIMD: Test where and conditional and any_of --- Tests/SIMD/main.cpp | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Tests/SIMD/main.cpp b/Tests/SIMD/main.cpp index 1d75f8b7c31..677442ad98f 100644 --- a/Tests/SIMD/main.cpp +++ b/Tests/SIMD/main.cpp @@ -425,6 +425,50 @@ int main (int argc, char* argv[]) << (err == 0 ? "PASSED" : "FAILED") << "\n"; } + // ================================================================ + // Test 14: any_of, where, condition — portable single-source + // Uses SIMDReal<>, which is a SIMD vector when AMREX_USE_SIMD=ON + // and a plain scalar when OFF. The same code path exercises + // both the real SIMD and the scalar fallback implementations. + // ================================================================ + { + using Real_t = simd::SIMDReal<>; + + // safe reciprocal: 1/b where b != 0, else 0 + Real_t b(ParticleReal(2)); + auto mask = b != Real_t(ParticleReal(0)); + auto safe_b = simd::condition(mask, b, Real_t(ParticleReal(1))); + auto recip = simd::condition(mask, + Real_t(ParticleReal(1)) / safe_b, + Real_t(ParticleReal(0))); + + // any_of: at least one lane should be nonzero + AMREX_ALWAYS_ASSERT(simd::stdx::any_of(mask)); + + // where: masked assignment + Real_t acc(ParticleReal(0)); + simd::stdx::where(mask, acc) = recip; + + // verify: b=2 everywhere → recip=0.5, acc=0.5 + int err = 0; + auto check = [&] (ParticleReal got, ParticleReal expected) { + if (std::abs(got - expected) > ParticleReal(1.e-10)) { ++err; } + }; +#ifdef AMREX_USE_SIMD + for (int lane = 0; lane < static_cast(Real_t::size()); ++lane) { + check(recip[lane], ParticleReal(0.5)); + check(acc[lane], ParticleReal(0.5)); + } +#else + check(recip, ParticleReal(0.5)); + check(acc, ParticleReal(0.5)); +#endif + + nerrors += err; + Print() << "any_of + where + condition (portable): " + << (err == 0 ? "PASSED" : "FAILED") << "\n"; + } + // ================================================================ // Final report // ================================================================ From e1f16414908e360aee0a476ba62ec2b40b55f45f Mon Sep 17 00:00:00 2001 From: Axel Huebl Date: Thu, 26 Feb 2026 11:28:44 -0800 Subject: [PATCH 3/6] Rename `condition` -> `select` This is the likely ISO C++26 name. --- Src/Base/AMReX_SIMD.H | 12 +++++++----- Tests/SIMD/main.cpp | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Src/Base/AMReX_SIMD.H b/Src/Base/AMReX_SIMD.H index 821ef2e723c..f8b208ea830 100644 --- a/Src/Base/AMReX_SIMD.H +++ b/Src/Base/AMReX_SIMD.H @@ -214,7 +214,7 @@ namespace amrex::simd return val_arr[n]; } - /** Vectorized ternary operator: condition(mask, true_val, false_val) + /** Vectorized ternary operator: select(mask, true_val, false_val) * * Selects elements from true_val where mask is true and from false_val where * mask is false. Analogous to (mask ? true_val : false_val) for scalars. @@ -228,15 +228,17 @@ namespace amrex::simd * template * T compute (T const& a, T const& b) * { - * auto safe_b = amrex::simd::condition(b != T(0), b, T(1)); - * return amrex::simd::condition(b != T(0), a / safe_b, T(0)); + * auto safe_b = amrex::simd::select(b != T(0), b, T(1)); + * return amrex::simd::select(b != T(0), a / safe_b, T(0)); * } * ``` + * + * @see C++26 std::datapar::select */ #ifdef AMREX_USE_SIMD template AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE - stdx::simd condition ( + stdx::simd select ( typename stdx::simd::mask_type const& mask, stdx::simd const& true_val, stdx::simd const& false_val) @@ -248,7 +250,7 @@ namespace amrex::simd #else template AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE - T condition (bool const mask, T const& true_val, T const& false_val) + T select (bool const mask, T const& true_val, T const& false_val) { return mask ? true_val : false_val; } diff --git a/Tests/SIMD/main.cpp b/Tests/SIMD/main.cpp index 677442ad98f..774a24bae47 100644 --- a/Tests/SIMD/main.cpp +++ b/Tests/SIMD/main.cpp @@ -437,8 +437,8 @@ int main (int argc, char* argv[]) // safe reciprocal: 1/b where b != 0, else 0 Real_t b(ParticleReal(2)); auto mask = b != Real_t(ParticleReal(0)); - auto safe_b = simd::condition(mask, b, Real_t(ParticleReal(1))); - auto recip = simd::condition(mask, + auto safe_b = simd::select(mask, b, Real_t(ParticleReal(1))); + auto recip = simd::select(mask, Real_t(ParticleReal(1)) / safe_b, Real_t(ParticleReal(0))); @@ -465,7 +465,7 @@ int main (int argc, char* argv[]) #endif nerrors += err; - Print() << "any_of + where + condition (portable): " + Print() << "any_of + where + select (portable): " << (err == 0 ? "PASSED" : "FAILED") << "\n"; } From 888c531db69ccb75516f19162edc1ea3988d66a2 Mon Sep 17 00:00:00 2001 From: Axel Huebl Date: Thu, 26 Feb 2026 11:34:46 -0800 Subject: [PATCH 4/6] Move `select` to `amrex::simd::stdx` Will be there eventually with C++26 --- Src/Base/AMReX_SIMD.H | 92 +++++++++++++++++++++++-------------------- Tests/SIMD/main.cpp | 6 +-- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/Src/Base/AMReX_SIMD.H b/Src/Base/AMReX_SIMD.H index f8b208ea830..a6075c3d7e6 100644 --- a/Src/Base/AMReX_SIMD.H +++ b/Src/Base/AMReX_SIMD.H @@ -28,6 +28,44 @@ namespace amrex::simd # if __cplusplus >= 202002L using vir::cvt; # endif + + /** Vectorized ternary operator: select(mask, true_val, false_val) + * + * Selects elements from true_val where mask is true and from false_val + * where mask is false. Analogous to (mask ? true_val : false_val) for + * scalars. + * + * Note: both true_val and false_val are eagerly evaluated (function + * arguments). To guard against operations like division by zero, + * sanitize inputs before the operation rather than relying on + * conditional selection. + * + * Example: + * ```cpp + * template + * T compute (T const& a, T const& b) + * { + * auto safe_b = amrex::simd::stdx::select(b != T(0), b, T(1)); + * return amrex::simd::stdx::select(b != T(0), a / safe_b, T(0)); + * } + * ``` + * + * @see C++26 std::simd select + * + * @todo Remove when SIMD provider (vir-simd / C++26) provides select. + * https://github.com/mattkretz/vir-simd/issues/49 + */ + template + AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE + vir::stdx::simd select ( + typename vir::stdx::simd::mask_type const& mask, + vir::stdx::simd const& true_val, + vir::stdx::simd const& false_val) + { + vir::stdx::simd result = false_val; + where(mask, result) = true_val; + return result; + } #else // fallback implementations for functions that are commonly used in portable code paths @@ -82,6 +120,18 @@ namespace amrex::simd { return {mask, value}; } + + /** Vectorized ternary operator (scalar fallback for simd select) + * + * @see select in the AMREX_USE_SIMD path above + * @see C++26 std::simd select + */ + template + AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE + T select (bool const mask, T const& true_val, T const& false_val) + { + return mask ? true_val : false_val; + } #endif } @@ -214,48 +264,6 @@ namespace amrex::simd return val_arr[n]; } - /** Vectorized ternary operator: select(mask, true_val, false_val) - * - * Selects elements from true_val where mask is true and from false_val where - * mask is false. Analogous to (mask ? true_val : false_val) for scalars. - * - * Note: both true_val and false_val are eagerly evaluated (function arguments). - * To guard against operations like division by zero, sanitize inputs before - * the operation rather than relying on conditional selection. - * - * Example: - * ```cpp - * template - * T compute (T const& a, T const& b) - * { - * auto safe_b = amrex::simd::select(b != T(0), b, T(1)); - * return amrex::simd::select(b != T(0), a / safe_b, T(0)); - * } - * ``` - * - * @see C++26 std::datapar::select - */ -#ifdef AMREX_USE_SIMD - template - AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE - stdx::simd select ( - typename stdx::simd::mask_type const& mask, - stdx::simd const& true_val, - stdx::simd const& false_val) - { - stdx::simd result = false_val; - stdx::where(mask, result) = true_val; - return result; - } -#else - template - AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE - T select (bool const mask, T const& true_val, T const& false_val) - { - return mask ? true_val : false_val; - } -#endif - /** Load 1D contiguous data from array pointers * * On GPU and CPU w/o SIMD, this dereferences a 1D array element at the diff --git a/Tests/SIMD/main.cpp b/Tests/SIMD/main.cpp index 774a24bae47..edfa7795cbe 100644 --- a/Tests/SIMD/main.cpp +++ b/Tests/SIMD/main.cpp @@ -426,7 +426,7 @@ int main (int argc, char* argv[]) } // ================================================================ - // Test 14: any_of, where, condition — portable single-source + // Test 14: any_of, where, select — portable single-source // Uses SIMDReal<>, which is a SIMD vector when AMREX_USE_SIMD=ON // and a plain scalar when OFF. The same code path exercises // both the real SIMD and the scalar fallback implementations. @@ -437,8 +437,8 @@ int main (int argc, char* argv[]) // safe reciprocal: 1/b where b != 0, else 0 Real_t b(ParticleReal(2)); auto mask = b != Real_t(ParticleReal(0)); - auto safe_b = simd::select(mask, b, Real_t(ParticleReal(1))); - auto recip = simd::select(mask, + auto safe_b = simd::stdx::select(mask, b, Real_t(ParticleReal(1))); + auto recip = simd::stdx::select(mask, Real_t(ParticleReal(1)) / safe_b, Real_t(ParticleReal(0))); From 98e25d70515d1f4ab8eec75f23ad9231886ee000 Mon Sep 17 00:00:00 2001 From: Axel Huebl Date: Thu, 26 Feb 2026 12:54:16 -0800 Subject: [PATCH 5/6] Cleaning for Warnings & Core Guidelines --- Src/Base/AMReX_SIMD.H | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Src/Base/AMReX_SIMD.H b/Src/Base/AMReX_SIMD.H index a6075c3d7e6..bce96d563af 100644 --- a/Src/Base/AMReX_SIMD.H +++ b/Src/Base/AMReX_SIMD.H @@ -88,12 +88,13 @@ namespace amrex::simd template struct where_expression { bool mask; - T& value; + T* value; AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE - void operator= (T const& new_val) + where_expression& operator= (T const& new_val) { - if (mask) { value = new_val; } + if (mask) { *value = new_val; } + return *this; } }; } @@ -118,7 +119,7 @@ namespace amrex::simd AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE detail::where_expression where (bool const mask, T& value) { - return {mask, value}; + return {mask, &value}; } /** Vectorized ternary operator (scalar fallback for simd select) From f26ead9485688e570238cf2b243c05064c85cdfb Mon Sep 17 00:00:00 2001 From: Axel Huebl Date: Mon, 2 Mar 2026 12:44:33 -0800 Subject: [PATCH 6/6] Test: Fixed Mixed Precision Consistently: Real -> ParticleReal --- Tests/SIMD/main.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/SIMD/main.cpp b/Tests/SIMD/main.cpp index edfa7795cbe..cfd1dd5d51f 100644 --- a/Tests/SIMD/main.cpp +++ b/Tests/SIMD/main.cpp @@ -427,26 +427,26 @@ int main (int argc, char* argv[]) // ================================================================ // Test 14: any_of, where, select — portable single-source - // Uses SIMDReal<>, which is a SIMD vector when AMREX_USE_SIMD=ON + // Uses SIMDParticleReal<>, which is a SIMD vector when AMREX_USE_SIMD=ON // and a plain scalar when OFF. The same code path exercises // both the real SIMD and the scalar fallback implementations. // ================================================================ { - using Real_t = simd::SIMDReal<>; + using PReal_t = simd::SIMDParticleReal<>; // safe reciprocal: 1/b where b != 0, else 0 - Real_t b(ParticleReal(2)); - auto mask = b != Real_t(ParticleReal(0)); - auto safe_b = simd::stdx::select(mask, b, Real_t(ParticleReal(1))); + auto b = PReal_t(2); + auto mask = b != PReal_t(0); + auto safe_b = simd::stdx::select(mask, b, PReal_t(1)); auto recip = simd::stdx::select(mask, - Real_t(ParticleReal(1)) / safe_b, - Real_t(ParticleReal(0))); + PReal_t(1) / safe_b, + PReal_t(0)); // any_of: at least one lane should be nonzero AMREX_ALWAYS_ASSERT(simd::stdx::any_of(mask)); // where: masked assignment - Real_t acc(ParticleReal(0)); + auto acc = PReal_t(0); simd::stdx::where(mask, acc) = recip; // verify: b=2 everywhere → recip=0.5, acc=0.5 @@ -455,7 +455,7 @@ int main (int argc, char* argv[]) if (std::abs(got - expected) > ParticleReal(1.e-10)) { ++err; } }; #ifdef AMREX_USE_SIMD - for (int lane = 0; lane < static_cast(Real_t::size()); ++lane) { + for (int lane = 0; lane < static_cast(PReal_t::size()); ++lane) { check(recip[lane], ParticleReal(0.5)); check(acc[lane], ParticleReal(0.5)); }