#if !defined(TORCH_STABLE_ONLY) && !defined(TORCH_TARGET_VERSION) #pragma once #include #include namespace c10 { namespace metal { namespace detail { template struct simd_type { using t = T; }; // Helper that allows one to run simd ops over bfl16 by upcasting them to fp32 template using simd_type_t = typename simd_type::t; template <> struct simd_type { using t = float; }; } // namespace detail template inline ::metal:: enable_if_t && !c10::metal::is_complex_v, T> simd_sum(T val) { return T(::metal::simd_sum(detail::simd_type_t(val))); } inline float2 simd_sum(float2 val) { return float2(::metal::simd_sum(val.x), ::metal::simd_sum(val.y)); } template inline ::metal:: enable_if_t && !c10::metal::is_complex_v, T> simd_prod(T val) { return T(::metal::simd_product(detail::simd_type_t(val))); } // Complex product reduction via shuffle, using c10::metal::mul for (a+bi)(c+di) // Uses simd_shuffle_and_fill_down with identity (1+0i) for inactive lanes. inline float2 simd_prod(float2 val) { for (ushort i = simdgroup_size / 2; i > 0; i /= 2) { val = c10::metal::mul( val, ::metal::simd_shuffle_and_fill_down(val, float2(1, 0), i)); } return val; } // Extend simd_broadcast to 64-bit integral types using int2 trick template < typename T, ::metal::enable_if_t<::metal::is_integral_v && sizeof(T) == 8, bool> = true> inline T simd_broadcast(T val, ushort lane_id) { return as_type(::metal::simd_broadcast(as_type(val), lane_id)); } template < typename T, ::metal::enable_if_t || sizeof(T) != 8, bool> = true> inline T simd_broadcast(T val, ushort lane_id) { return ::metal::simd_broadcast(val, lane_id); } // Floating simd_min/max with nan propagation template < typename T, ::metal::enable_if_t<::metal::is_floating_point_v, bool> = true> inline T simd_max(T val) { if (::metal::simd_any(::metal::isnan(val))) { return ::metal::numeric_limits::quiet_NaN(); } return T(::metal::simd_max(detail::simd_type_t(val))); } template < typename T, ::metal::enable_if_t<::metal::is_floating_point_v, bool> = true> inline T simd_min(T val) { if (::metal::simd_any(::metal::isnan(val))) { return ::metal::numeric_limits::quiet_NaN(); } return T(::metal::simd_min(detail::simd_type_t(val))); } template < typename T, ::metal::enable_if_t<::metal::is_integral_v && sizeof(T) != 8, bool> = true> inline T simd_max(T val) { return ::metal::simd_max(val); } template < typename T, ::metal::enable_if_t<::metal::is_integral_v && sizeof(T) != 8, bool> = true> inline T simd_min(T val) { return ::metal::simd_min(val); } // Metal does not support SIMD reductions over 64-bit types, but it could be // implement using simd_shuffle_down, that yields result in log2(simdgroup_size) // iterations Use fill variant, as shuffle down returns garbage if inactive // thread is referenced (on M1/M2, works fine on M4) and broadcast result to all // threads in the end. Implementation heavily borrows from // https://github.com/ml-explore/mlx/blob/86389bf9707f46101af45d90510e8e97c8a90b93/mlx/backend/metal/kernels/reduction/ops.h#L16 template inline ::metal::enable_if_t<::metal::is_same_v, T> simd_sum(T val) { for (ushort i = simdgroup_size / 2; i > 0; i /= 2) { val += as_type( ::metal::simd_shuffle_and_fill_down(as_type(val), int2(0), i)); } return simd_broadcast(val, 0); } template inline ::metal::enable_if_t<::metal::is_same_v, T> simd_prod(T val) { for (ushort i = simdgroup_size / 2; i > 0; i /= 2) { val *= as_type( ::metal::simd_shuffle_and_fill_down(as_type(val), int2(0), i)); } return simd_broadcast(val, 0); } // Fill value for shuffle_and_fill_down must be the op's identity (the fill // is contributed by lanes whose shuffle target is past simdgroup_size). // // NOTE: callers with fewer than simdgroup_size active lanes still risk // corruption -- an active lane reading from an in-range but inactive lane // gets undefined data (0 in practice on Apple Silicon), not the fill. // Round up the dispatched thread count to a multiple of simdgroup_size // and have padding threads load the op identity before calling this. template inline ::metal::enable_if_t<::metal::is_same_v, T> simd_max(T val) { const auto fill = as_type(::metal::numeric_limits::lowest()); for (ushort i = simdgroup_size / 2; i > 0; i /= 2) { val = ::metal::max( val, as_type( ::metal::simd_shuffle_and_fill_down(as_type(val), fill, i))); } return simd_broadcast(val, 0); } template inline ::metal::enable_if_t<::metal::is_same_v, T> simd_min(T val) { const auto fill = as_type(::metal::numeric_limits::max()); for (ushort i = simdgroup_size / 2; i > 0; i /= 2) { val = ::metal::min( val, as_type( ::metal::simd_shuffle_and_fill_down(as_type(val), fill, i))); } return simd_broadcast(val, 0); } // argmin/argmax helpers using simd_ballot template < typename T, ::metal::enable_if_t<::metal::is_integral_v, bool> = true> inline ::c10::metal::pair simd_argmin(T val) { const auto rc = simd_min(val); const auto vote = ::metal::simd_ballot(val == rc); return {rc, static_cast(::metal::ctz(static_cast(vote)))}; } template < typename T, ::metal::enable_if_t<::metal::is_floating_point_v, bool> = true> inline ::c10::metal::pair simd_argmin(T val) { const auto rc = simd_min(val); const auto vote = ::metal::simd_ballot(val == rc || ::metal::isnan(val)); return {rc, static_cast(::metal::ctz(static_cast(vote)))}; } template < typename T, ::metal::enable_if_t<::metal::is_integral_v, bool> = true> inline ::c10::metal::pair simd_argmax(T val) { const auto rc = simd_max(val); const auto vote = ::metal::simd_ballot(val == rc); return {rc, static_cast(::metal::ctz(static_cast(vote)))}; } template < typename T, ::metal::enable_if_t<::metal::is_floating_point_v, bool> = true> inline ::c10::metal::pair simd_argmax(T val) { const auto rc = simd_max(val); const auto vote = ::metal::simd_ballot(val == rc || ::metal::isnan(val)); return {rc, static_cast(::metal::ctz(static_cast(vote)))}; } template inline c10::metal::pair simd_argmin(ARG_T val, IDX_T idx_val) { auto rc = simd_argmin(val); return {rc.first, simd_broadcast(idx_val, rc.second)}; } template inline c10::metal::pair simd_argmax(ARG_T val, IDX_T idx_val) { auto rc = simd_argmax(val); return {rc.first, simd_broadcast(idx_val, rc.second)}; } // Below algorithms are written with hardcoded assumption that simdgroup is 32 // and threadgroup_max is 1024, i.e. reduction can be done in two stages max template opmath_t threadgroup_sum( threadgroup opmath_t* data, T val, unsigned idx, unsigned size) { auto rc = simd_sum(static_cast>(val)); if (idx % simdgroup_size == 0) { data[idx / simdgroup_size] = rc; } if (size > simdgroup_size) { ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); if (idx < ((size + simdgroup_size - 1) / simdgroup_size)) { auto rc1 = simd_sum(data[idx]); if (idx == 0) { data[0] = rc1; } } } ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); return data[0]; } template ::metal::array, 2> threadgroup_sum2( threadgroup opmath_t* data_a, threadgroup opmath_t* data_b, T val_a, T val_b, unsigned idx, unsigned size) { auto rc_a = simd_sum(static_cast>(val_a)); auto rc_b = simd_sum(static_cast>(val_b)); if (idx % simdgroup_size == 0) { data_a[idx / simdgroup_size] = rc_a; data_b[idx / simdgroup_size] = rc_b; } if (size > simdgroup_size) { ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); if (idx < ((size + simdgroup_size - 1) / simdgroup_size)) { auto rc1_a = simd_sum(data_a[idx]); auto rc1_b = simd_sum(data_b[idx]); if (idx == 0) { data_a[0] = rc1_a; data_b[0] = rc1_b; } } } ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); return ::metal::array, 2>{data_a[0], data_b[0]}; } template opmath_t threadgroup_prod( threadgroup opmath_t* data, T val, unsigned idx, unsigned size) { auto rc = simd_prod(static_cast>(val)); if (idx % simdgroup_size == 0) { data[idx / simdgroup_size] = rc; } if (size > simdgroup_size) { ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); if (idx < ((size + simdgroup_size - 1) / simdgroup_size)) { auto rc1 = simd_prod(data[idx]); if (idx == 0) { data[0] = rc1; } } } ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); return data[0]; } template T threadgroup_max(threadgroup T* data, T val, unsigned idx, unsigned size) { auto rc = simd_max(val); if (idx % simdgroup_size == 0) { data[idx / simdgroup_size] = rc; } if (size > simdgroup_size) { ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); if (idx < ((size + simdgroup_size - 1) / simdgroup_size)) { auto rc1 = simd_max(data[idx]); if (idx == 0) { data[0] = rc1; } } } ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); return data[0]; } template T threadgroup_min(threadgroup T* data, T val, unsigned idx, unsigned size) { auto rc = simd_min(val); if (idx % simdgroup_size == 0) { data[idx / simdgroup_size] = rc; } if (size > simdgroup_size) { ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); if (idx < ((size + simdgroup_size - 1) / simdgroup_size)) { auto rc1 = simd_min(data[idx]); if (idx == 0) { data[0] = rc1; } } } ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); return data[0]; } template float3 threadgroup_welford_reduce(threadgroup T* data, unsigned size) { ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); float m = data[0]; float m2 = 0; for (unsigned idx = 1; idx < size; ++idx) { float delta = data[idx] - m; m += delta / (idx + 1); m2 += delta * (data[idx] - m); } ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); return float3(m, m2, size); } // Each vec3type is tuple of mean, m2 and weight template float3 welford_combine(T a, T b) { float delta = b.x - a.x; float new_weight = a.z + b.z; auto w2_over_w = new_weight != 0 ? b.z / new_weight : 0.0; return float3( a.x + delta * w2_over_w, a.y + b.y + delta * delta * a.z * w2_over_w, new_weight); } template float3 threadgroup_welford_combine(threadgroup T* data, unsigned size) { ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); float3 rc = data[0]; for (unsigned idx = 1; idx < size; ++idx) { rc = welford_combine(rc, data[idx]); } ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); return rc; } template IDX_T threadgroup_argmax( threadgroup ARG_T* arg_data, threadgroup IDX_T* idx_data, ARG_T val, IDX_T idx_val, unsigned idx, unsigned size) { auto rc = simd_argmax(val, idx_val); if (size <= simdgroup_size) { return rc.second; } if (idx % simdgroup_size == 0) { arg_data[idx / simdgroup_size] = rc.first; idx_data[idx / simdgroup_size] = rc.second; } ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); if (idx < ((size + simdgroup_size - 1) / simdgroup_size)) { auto rc1 = simd_argmax(arg_data[idx], idx_data[idx]); if (idx == 0) { idx_data[0] = rc1.second; } } ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); return idx_data[0]; } template IDX_T threadgroup_argmin( threadgroup ARG_T* arg_data, threadgroup IDX_T* idx_data, ARG_T val, IDX_T idx_val, unsigned idx, unsigned size) { auto rc = simd_argmin(val, idx_val); if (size <= simdgroup_size) { return rc.second; } if (idx % simdgroup_size == 0) { arg_data[idx / simdgroup_size] = rc.first; idx_data[idx / simdgroup_size] = rc.second; } ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); if (idx < ((size + simdgroup_size - 1) / simdgroup_size)) { auto rc1 = simd_argmin(arg_data[idx], idx_data[idx]); if (idx == 0) { idx_data[0] = rc1.second; } } ::metal::threadgroup_barrier(::metal::mem_flags::mem_threadgroup); return idx_data[0]; } // Reduction op functors bundling identity, replace predicate, and the value // reduction helpers (combine / simd_reduce / threadgroup_reduce) used by both // value and arg reductions. NaN handling: for floats, isnan-propagating // max/min and a strict "replace" predicate that lets NaN beat any finite // value and lets any finite value beat -INF/+INF identity. // // Float identity is +/-INFINITY (not numeric_limits::lowest/max, i.e. // -/+FLT_MAX): max(-INF, x) = x for any finite x including -FLT_MAX, but // max(-FLT_MAX, -INFINITY) would incorrectly return -FLT_MAX. // // The isnan() calls below route through static_cast() so the float // branch type-checks even when T is integral (Metal 3 lowers `if IF_CONSTEXPR` // to a runtime `if` and parses both arms). Both branches' bodies still get // dropped on Metal 4 where IF_CONSTEXPR is real `if constexpr`. template struct MaxOp { static inline constexpr T identity() { if IF_CONSTEXPR (::metal::is_floating_point_v) { return T(-INFINITY); } else { return ::metal::numeric_limits::lowest(); } } // Strict "should-replace" predicate: returns true iff cand strictly beats // cur. NaN-propagating: NaN cand beats finite cur; finite cand never beats // NaN cur. static inline bool replace(T cand, T cur) { if IF_CONSTEXPR (::metal::is_floating_point_v) { if (::metal::isnan(static_cast(cur))) { return false; } return ::metal::isnan(static_cast(cand)) || cand > cur; } return cand > cur; } static inline T combine(T a, T b) { return c10::metal::max(a, b); } static inline T simd_reduce(T val) { return c10::metal::simd_max(val); } static inline T threadgroup_reduce( threadgroup T* shared, T val, uint tid, uint tptg) { return c10::metal::threadgroup_max(shared, val, tid, tptg); } }; template struct MinOp { static inline constexpr T identity() { if IF_CONSTEXPR (::metal::is_floating_point_v) { return T(INFINITY); } else { return ::metal::numeric_limits::max(); } } static inline bool replace(T cand, T cur) { if IF_CONSTEXPR (::metal::is_floating_point_v) { if (::metal::isnan(static_cast(cur))) { return false; } return ::metal::isnan(static_cast(cand)) || cand < cur; } return cand < cur; } static inline T combine(T a, T b) { return c10::metal::min(a, b); } static inline T simd_reduce(T val) { return c10::metal::simd_min(val); } static inline T threadgroup_reduce( threadgroup T* shared, T val, uint tid, uint tptg) { return c10::metal::threadgroup_min(shared, val, tid, tptg); } }; } // namespace metal } // namespace c10 #else #error "This file should not be included when either TORCH_STABLE_ONLY or TORCH_TARGET_VERSION is defined." #endif // !defined(TORCH_STABLE_ONLY) && !defined(TORCH_TARGET_VERSION)