From 7f8e89223ba95967bdd432bdab74312dc7e18efd Mon Sep 17 00:00:00 2001 From: jabrailkhalil Date: Thu, 10 Sep 2026 22:25:21 +0300 Subject: [PATCH 1/2] Fix TensorPrimitives MinNumber/MaxNumber span reductions propagating NaN (#133346) --- .../TensorPrimitives.IAggregationOperator.cs | 9 +++ .../Tensors/netcore/TensorPrimitives.Max.cs | 22 +++--- .../TensorPrimitives.MaxMagnitudeNumber.cs | 2 + .../netcore/TensorPrimitives.MaxNumber.cs | 2 + .../TensorPrimitives.MinMagnitudeNumber.cs | 2 + .../netcore/TensorPrimitives.MinNumber.cs | 2 + .../tests/TensorPrimitives.Generic.cs | 71 +++++++++++++++++++ 7 files changed, 99 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Common/TensorPrimitives.IAggregationOperator.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Common/TensorPrimitives.IAggregationOperator.cs index f3115744143737..24fc6e58ab9956 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Common/TensorPrimitives.IAggregationOperator.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Common/TensorPrimitives.IAggregationOperator.cs @@ -19,6 +19,15 @@ private interface IAggregationOperator : IBinaryOperator static abstract T Invoke(Vector256 x); static abstract T Invoke(Vector512 x); + /// + /// Whether the operator propagates NaN inputs to its output, as the IEEE 754:2019 + /// minimum/maximum (and magnitude) functions do. Operators implementing the + /// minimumNumber/maximumNumber family return , so the + /// reduction does not early-exit on a NaN and the lane-wise operator gets to ignore it + /// when a numeric operand is available. + /// + static virtual bool PropagatesNaNs => true; + static virtual T IdentityValue => throw new NotSupportedException(); } diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Max.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Max.cs index 1e7249bd3fc751..1b98d228423d25 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Max.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Max.cs @@ -147,7 +147,7 @@ private static T MinMaxCore(ReadOnlySpan x) Vector512 current; Vector512 nanMask; - if (typeof(T) == typeof(float) || typeof(T) == typeof(double)) + if (TMinMaxOperator.PropagatesNaNs && (typeof(T) == typeof(float) || typeof(T) == typeof(double))) { // Check for NaNs nanMask = Vector512.IsNaN(result); @@ -166,7 +166,7 @@ private static T MinMaxCore(ReadOnlySpan x) // Load the next vector, and early exit on NaN. current = Vector512.LoadUnsafe(ref xRef, (uint)i); - if (typeof(T) == typeof(float) || typeof(T) == typeof(double)) + if (TMinMaxOperator.PropagatesNaNs && (typeof(T) == typeof(float) || typeof(T) == typeof(double))) { // Check for NaNs nanMask = ~Vector512.Equals(current, current); @@ -185,7 +185,7 @@ private static T MinMaxCore(ReadOnlySpan x) { current = Vector512.LoadUnsafe(ref xRef, (uint)(x.Length - Vector512.Count)); - if (typeof(T) == typeof(float) || typeof(T) == typeof(double)) + if (TMinMaxOperator.PropagatesNaNs && (typeof(T) == typeof(float) || typeof(T) == typeof(double))) { // Check for NaNs nanMask = ~Vector512.Equals(current, current); @@ -212,7 +212,7 @@ private static T MinMaxCore(ReadOnlySpan x) Vector256 current; Vector256 nanMask; - if (typeof(T) == typeof(float) || typeof(T) == typeof(double)) + if (TMinMaxOperator.PropagatesNaNs && (typeof(T) == typeof(float) || typeof(T) == typeof(double))) { // Check for NaNs nanMask = ~Vector256.Equals(result, result); @@ -231,7 +231,7 @@ private static T MinMaxCore(ReadOnlySpan x) // Load the next vector, and early exit on NaN. current = Vector256.LoadUnsafe(ref xRef, (uint)i); - if (typeof(T) == typeof(float) || typeof(T) == typeof(double)) + if (TMinMaxOperator.PropagatesNaNs && (typeof(T) == typeof(float) || typeof(T) == typeof(double))) { // Check for NaNs nanMask = ~Vector256.Equals(current, current); @@ -251,7 +251,7 @@ private static T MinMaxCore(ReadOnlySpan x) current = Vector256.LoadUnsafe(ref xRef, (uint)(x.Length - Vector256.Count)); - if (typeof(T) == typeof(float) || typeof(T) == typeof(double)) + if (TMinMaxOperator.PropagatesNaNs && (typeof(T) == typeof(float) || typeof(T) == typeof(double))) { // Check for NaNs nanMask = ~Vector256.Equals(current, current); @@ -278,7 +278,7 @@ private static T MinMaxCore(ReadOnlySpan x) Vector128 current; Vector128 nanMask; - if (typeof(T) == typeof(float) || typeof(T) == typeof(double)) + if (TMinMaxOperator.PropagatesNaNs && (typeof(T) == typeof(float) || typeof(T) == typeof(double))) { // Check for NaNs nanMask = Vector128.IsNaN(result); @@ -297,7 +297,7 @@ private static T MinMaxCore(ReadOnlySpan x) // Load the next vector, and early exit on NaN. current = Vector128.LoadUnsafe(ref xRef, (uint)i); - if (typeof(T) == typeof(float) || typeof(T) == typeof(double)) + if (TMinMaxOperator.PropagatesNaNs && (typeof(T) == typeof(float) || typeof(T) == typeof(double))) { // Check for NaNs nanMask = Vector128.IsNaN(current); @@ -316,7 +316,7 @@ private static T MinMaxCore(ReadOnlySpan x) { current = Vector128.LoadUnsafe(ref xRef, (uint)(x.Length - Vector128.Count)); - if (typeof(T) == typeof(float) || typeof(T) == typeof(double)) + if (TMinMaxOperator.PropagatesNaNs && (typeof(T) == typeof(float) || typeof(T) == typeof(double))) { // Check for NaNs nanMask = Vector128.IsNaN(current); @@ -335,7 +335,7 @@ private static T MinMaxCore(ReadOnlySpan x) // Scalar path used when either vectorization is not supported or the input is too small to vectorize. T curResult = x[0]; - if (T.IsNaN(curResult)) + if (TMinMaxOperator.PropagatesNaNs && T.IsNaN(curResult)) { return curResult; } @@ -343,7 +343,7 @@ private static T MinMaxCore(ReadOnlySpan x) for (int i = 1; i < x.Length; i++) { T current = x[i]; - if (T.IsNaN(current)) + if (TMinMaxOperator.PropagatesNaNs && T.IsNaN(current)) { return current; } diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MaxMagnitudeNumber.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MaxMagnitudeNumber.cs index 070c56c1c60630..817d2cea36a8d7 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MaxMagnitudeNumber.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MaxMagnitudeNumber.cs @@ -105,6 +105,8 @@ public static void MaxMagnitudeNumber(ReadOnlySpan x, T y, Span destina { public static bool Vectorizable => true; + public static bool PropagatesNaNs => false; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Invoke(T x, T y) => T.MaxMagnitudeNumber(x, y); diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MaxNumber.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MaxNumber.cs index 53e82a1975f51a..b24b313e744b41 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MaxNumber.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MaxNumber.cs @@ -99,6 +99,8 @@ public static void MaxNumber(ReadOnlySpan x, T y, Span destination) { public static bool Vectorizable => true; + public static bool PropagatesNaNs => false; + public static T Invoke(T x, T y) => T.MaxNumber(x, y); [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MinMagnitudeNumber.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MinMagnitudeNumber.cs index 86ed39d37aa08b..3e0f3712f22f25 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MinMagnitudeNumber.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MinMagnitudeNumber.cs @@ -105,6 +105,8 @@ public static void MinMagnitudeNumber(ReadOnlySpan x, T y, Span destina { public static bool Vectorizable => true; + public static bool PropagatesNaNs => false; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Invoke(T x, T y) => T.MinMagnitudeNumber(x, y); diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MinNumber.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MinNumber.cs index 3afa7b80888e7c..b94c6af8a224c0 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MinNumber.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.MinNumber.cs @@ -99,6 +99,8 @@ public static void MinNumber(ReadOnlySpan x, T y, Span destination) { public static bool Vectorizable => true; + public static bool PropagatesNaNs => false; + public static T Invoke(T x, T y) => T.MinNumber(x, y); [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs index 295955d9497ea6..4ebc5c044b427e 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs @@ -1772,6 +1772,77 @@ public void StdDev_AllLengths() }); } #endregion + + #region Number aggregates ignore NaN + [Theory] + [InlineData(1)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(16)] + [InlineData(33)] + public void NumberAggregates_IgnoreNaN(int length) + { + // IEEE 754:2019 minimumNumber/maximumNumber ignore a NaN operand when a numeric one is + // available, while minimum/maximum propagate it. The span reductions must match. + T nan = T.CreateTruncating(float.NaN); + T one = T.One; + T two = one + one; + + if (length == 1) + { + AssertEqualAggregate(nan, TensorPrimitives.MinNumber([nan])); + AssertEqualAggregate(nan, TensorPrimitives.MaxNumber([nan])); + AssertEqualAggregate(nan, TensorPrimitives.MinMagnitudeNumber([nan])); + AssertEqualAggregate(nan, TensorPrimitives.MaxMagnitudeNumber([nan])); + AssertEqualAggregate(nan, TensorPrimitives.Min([nan])); + AssertEqualAggregate(nan, TensorPrimitives.Max([nan])); + return; + } + + T[] values = new T[length]; + + // NaN at the start, in the middle, and at the end of the span, so both the vectorized + // and the scalar tails of the reduction see it. + foreach (int nanIndex in new[] { 0, length / 2, length - 1 }) + { + Array.Fill(values, two); + values[nanIndex] = nan; + + // A distinct minimum so the reduction is not trivially the fill value. + values[(nanIndex + 1) % length] = one; + + // Number variants ignore the NaN and pick the numeric extreme. + AssertEqualAggregate(one, TensorPrimitives.MinNumber(values)); + AssertEqualAggregate(two, TensorPrimitives.MaxNumber(values)); + AssertEqualAggregate(one, TensorPrimitives.MinMagnitudeNumber(values)); + AssertEqualAggregate(two, TensorPrimitives.MaxMagnitudeNumber(values)); + + // Plain Min/Max still propagate NaN. + AssertEqualAggregate(nan, TensorPrimitives.Min(values)); + AssertEqualAggregate(nan, TensorPrimitives.Max(values)); + AssertEqualAggregate(nan, TensorPrimitives.MinMagnitude(values)); + AssertEqualAggregate(nan, TensorPrimitives.MaxMagnitude(values)); + } + + // Signed zeros follow minimumNumber/maximumNumber: +0 is greater than -0. + T[] signedZeros = { -T.Zero, T.Zero }; + Assert.True(T.IsNegative(TensorPrimitives.MinNumber(signedZeros))); + Assert.False(T.IsNegative(TensorPrimitives.MaxNumber(signedZeros))); + + static void AssertEqualAggregate(T expected, T actual) + { + if (T.IsNaN(expected)) + { + Assert.True(T.IsNaN(actual), $"expected NaN, got {actual}"); + } + else + { + Assert.Equal(expected, actual); + } + } + } + #endregion } public unsafe abstract class GenericSignedIntegerTensorPrimitivesTests : GenericIntegerTensorPrimitivesTests From 8710e9a362e0e6b351992c0c6a8219f59f7af77f Mon Sep 17 00:00:00 2001 From: Jabrail Khalilov <78273416+jabrailkhalil@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:02:18 +0000 Subject: [PATCH 2/2] tests: expand Number aggregate coverage --- .../tests/TensorPrimitives.Generic.cs | 112 ++++++++++-------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs index 4ebc5c044b427e..80b2958825f635 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs @@ -1773,74 +1773,84 @@ public void StdDev_AllLengths() } #endregion - #region Number aggregates ignore NaN - [Theory] - [InlineData(1)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - [InlineData(16)] - [InlineData(33)] - public void NumberAggregates_IgnoreNaN(int length) + #region Number aggregates + [Fact] + public void NumberAggregates_AllLengths() + { + Assert.All(Helpers.TensorLengths, length => + { + T[] values = new T[length]; + for (int i = 0; i < length; i++) + { + T value = T.CreateChecked(i + 1); + values[i] = (i & 1) == 0 ? value : -value; + } + + T expectedMinNumber = values[0]; + T expectedMaxNumber = values[0]; + T expectedMinMagnitudeNumber = values[0]; + T expectedMaxMagnitudeNumber = values[0]; + + for (int i = 1; i < values.Length; i++) + { + expectedMinNumber = T.MinNumber(expectedMinNumber, values[i]); + expectedMaxNumber = T.MaxNumber(expectedMaxNumber, values[i]); + expectedMinMagnitudeNumber = T.MinMagnitudeNumber(expectedMinMagnitudeNumber, values[i]); + expectedMaxMagnitudeNumber = T.MaxMagnitudeNumber(expectedMaxMagnitudeNumber, values[i]); + } + + Assert.Equal(expectedMinNumber, TensorPrimitives.MinNumber(values)); + Assert.Equal(expectedMaxNumber, TensorPrimitives.MaxNumber(values)); + Assert.Equal(expectedMinMagnitudeNumber, TensorPrimitives.MinMagnitudeNumber(values)); + Assert.Equal(expectedMaxMagnitudeNumber, TensorPrimitives.MaxMagnitudeNumber(values)); + }); + } + + [Fact] + public void NumberAggregates_IgnoreNaN_AllLengths() { // IEEE 754:2019 minimumNumber/maximumNumber ignore a NaN operand when a numeric one is - // available, while minimum/maximum propagate it. The span reductions must match. + // available. Exercise every reduction length so scalar, Vector128, Vector256, Vector512, + // and scalar-tail paths are covered where supported. T nan = T.CreateTruncating(float.NaN); T one = T.One; T two = one + one; - if (length == 1) + Assert.All(Helpers.TensorLengths, length => { - AssertEqualAggregate(nan, TensorPrimitives.MinNumber([nan])); - AssertEqualAggregate(nan, TensorPrimitives.MaxNumber([nan])); - AssertEqualAggregate(nan, TensorPrimitives.MinMagnitudeNumber([nan])); - AssertEqualAggregate(nan, TensorPrimitives.MaxMagnitudeNumber([nan])); - AssertEqualAggregate(nan, TensorPrimitives.Min([nan])); - AssertEqualAggregate(nan, TensorPrimitives.Max([nan])); - return; - } + T[] allNaNs = new T[length]; + Array.Fill(allNaNs, nan); - T[] values = new T[length]; + Assert.Equal(nan, TensorPrimitives.MinNumber(allNaNs)); + Assert.Equal(nan, TensorPrimitives.MaxNumber(allNaNs)); + Assert.Equal(nan, TensorPrimitives.MinMagnitudeNumber(allNaNs)); + Assert.Equal(nan, TensorPrimitives.MaxMagnitudeNumber(allNaNs)); - // NaN at the start, in the middle, and at the end of the span, so both the vectorized - // and the scalar tails of the reduction see it. - foreach (int nanIndex in new[] { 0, length / 2, length - 1 }) - { - Array.Fill(values, two); - values[nanIndex] = nan; + if (length == 1) + { + return; + } - // A distinct minimum so the reduction is not trivially the fill value. - values[(nanIndex + 1) % length] = one; + T[] values = new T[length]; + T expectedMaximum = length > 2 ? two : one; - // Number variants ignore the NaN and pick the numeric extreme. - AssertEqualAggregate(one, TensorPrimitives.MinNumber(values)); - AssertEqualAggregate(two, TensorPrimitives.MaxNumber(values)); - AssertEqualAggregate(one, TensorPrimitives.MinMagnitudeNumber(values)); - AssertEqualAggregate(two, TensorPrimitives.MaxMagnitudeNumber(values)); + foreach (int nanIndex in new[] { 0, length / 2, length - 1 }) + { + Array.Fill(values, two); + values[nanIndex] = nan; + values[(nanIndex + 1) % length] = one; - // Plain Min/Max still propagate NaN. - AssertEqualAggregate(nan, TensorPrimitives.Min(values)); - AssertEqualAggregate(nan, TensorPrimitives.Max(values)); - AssertEqualAggregate(nan, TensorPrimitives.MinMagnitude(values)); - AssertEqualAggregate(nan, TensorPrimitives.MaxMagnitude(values)); - } + Assert.Equal(one, TensorPrimitives.MinNumber(values)); + Assert.Equal(expectedMaximum, TensorPrimitives.MaxNumber(values)); + Assert.Equal(one, TensorPrimitives.MinMagnitudeNumber(values)); + Assert.Equal(expectedMaximum, TensorPrimitives.MaxMagnitudeNumber(values)); + } + }); // Signed zeros follow minimumNumber/maximumNumber: +0 is greater than -0. T[] signedZeros = { -T.Zero, T.Zero }; Assert.True(T.IsNegative(TensorPrimitives.MinNumber(signedZeros))); Assert.False(T.IsNegative(TensorPrimitives.MaxNumber(signedZeros))); - - static void AssertEqualAggregate(T expected, T actual) - { - if (T.IsNaN(expected)) - { - Assert.True(T.IsNaN(actual), $"expected NaN, got {actual}"); - } - else - { - Assert.Equal(expected, actual); - } - } } #endregion }