From 186814d667c6af9a4d14ec6659dd672fae78f80e Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Tue, 29 Sep 2026 19:28:40 -0700 Subject: [PATCH 1/6] Fix Tensor shape and storage correctness Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../System.Numerics.Tensors/README.md | 26 + .../src/Resources/Strings.resx | 9 + .../Tensors/netcore/IReadOnlyTensor_1.cs | 14 +- .../netcore/ReadOnlyTensorDimensionSpan_1.cs | 2 +- .../Tensors/netcore/ReadOnlyTensorSpan_1.cs | 44 +- .../System/Numerics/Tensors/netcore/Tensor.cs | 645 +++++++++------ .../Tensors/netcore/Tensor.op_BitwiseAnd.cs | 2 +- .../Tensors/netcore/Tensor.op_BitwiseOr.cs | 2 +- .../Tensors/netcore/Tensor.op_ExclusiveOr.cs | 2 +- .../Tensors/netcore/TensorDimensionSpan_1.cs | 4 +- .../Tensors/netcore/TensorOperation.cs | 364 ++++++--- .../Numerics/Tensors/netcore/TensorShape.cs | 181 ++++- .../Numerics/Tensors/netcore/TensorSpan_1.cs | 43 +- .../Numerics/Tensors/netcore/Tensor_1.cs | 47 +- .../src/System/ThrowHelper.cs | 32 +- .../tests/ReadOnlyTensorSpanTests.cs | 226 +++++- .../tests/TensorDimensionSpanTests.cs | 52 ++ .../tests/TensorSpanTests.cs | 152 +++- .../tests/TensorTests.cs | 753 ++++++++++++++++++ 19 files changed, 2143 insertions(+), 457 deletions(-) diff --git a/src/libraries/System.Numerics.Tensors/README.md b/src/libraries/System.Numerics.Tensors/README.md index 6190da60e77c51..ddcf6ca4fd9ee8 100644 --- a/src/libraries/System.Numerics.Tensors/README.md +++ b/src/libraries/System.Numerics.Tensors/README.md @@ -1,3 +1,29 @@ # System.Numerics.Tensors Provides APIs for performing primitive operations over tensors represented by spans of memory. + +Some shape and storage behavior intentionally differs from NumPy: + +- Creating a tensor from an empty shape (`[]`) produces shape `[0]`, with no elements. In NumPy, + shape `()` has rank zero but contains one scalar element; `np.empty(())` leaves that element + uninitialized ("empty" refers to initialization, not the number of elements). +- Squeezing shape `[1, 1]` produces shape `[1]`, rather than NumPy's rank-zero shape `()`. + Both contain one element and can broadcast as a scalar; code that inspects rank, selects + axes, or indexes the result must account for the retained dimension. +- Strides must be nonnegative. NumPy can represent a reversed view with a negative stride + (for example, `array[::-1]`); use `Tensor.ReverseDimension` with dimension `0` to + reverse the first axis instead. This produces a new tensor, not a reversed view. +- When growing a tensor, `Tensor.Resize` and `Tensor.ResizeTo` fill new elements with + `default(T)`. Resizing `[1, 2]` to five `int` elements yields `[1, 2, 0, 0, 0]`, + whereas `np.resize` repeats the input and yields `[1, 2, 1, 2, 1]`. +- `Tensor.ResizeTo` and concatenation into an existing destination reject a zero-stride + dimension with more than one logical element: multiple output indexes would refer to the + same storage and could not hold distinct values. Zero strides in singleton dimensions + and empty destinations do not have this conflict. + +Overlapping sources and destinations are supported for equal-length dense copies, which +use the same overlap-safe behavior as `Span.CopyTo`, and for elementwise operations on identical +non-broadcast views. An in-place reversal of a dense tensor also needs no temporary +storage. Other overlapping tensor layouts throw `ArgumentException` before writing: +copying them correctly could require buffering an amount of data proportional to the +tensor's size. Nonoverlapping strided copies do not create such a buffer. diff --git a/src/libraries/System.Numerics.Tensors/src/Resources/Strings.resx b/src/libraries/System.Numerics.Tensors/src/Resources/Strings.resx index 641abff7c955bb..4d23be5dda037e 100644 --- a/src/libraries/System.Numerics.Tensors/src/Resources/Strings.resx +++ b/src/libraries/System.Numerics.Tensors/src/Resources/Strings.resx @@ -129,6 +129,9 @@ The destination span may only overlap with an input span if the two spans start at the same memory location. + + The input and destination tensor spans overlap in an unsupported layout. + Destination spans must not overlap with each other. @@ -138,6 +141,9 @@ The value '{0}' is not valid for this usage of the type {1}. + + Enumeration has not started or has already finished. + The type '{0}' is not supported because it contains references. @@ -168,6 +174,9 @@ Number of dimensions to slice does not equal the number of dimensions in the span + + The destination contains overlapping elements and cannot represent distinct output values. + The total length of the filter tensor must equal the length of the tensor to be filtered. diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/IReadOnlyTensor_1.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/IReadOnlyTensor_1.cs index 2a4abb58941a15..10acfdbf8c3462 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/IReadOnlyTensor_1.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/IReadOnlyTensor_1.cs @@ -49,14 +49,14 @@ public interface IReadOnlyTensor : IReadOnlyTensor /// Copies the contents of the tensor into a destination tensor span. /// The destination tensor span. - /// is shorter than the source tensor. - /// This method copies all of the source tensor to even if they overlap. + /// is shorter than the source tensor, or the source and destination overlap in an unsupported layout. + /// Overlapping dense tensors with equal element counts and identical views are supported. Other overlapping layouts are rejected before copying. void CopyTo(scoped in TensorSpan destination); /// Flattens the contents of the tensor into a destination span. /// The destination span. - /// is shorter than the source tensor. - /// This method copies all of the source tensor to even if they overlap. + /// is shorter than the source tensor, or the source and destination overlap in an unsupported layout. + /// Overlapping dense tensors are supported. Other overlapping layouts are rejected before copying. void FlattenTo(scoped Span destination); /// Returns a span that can be used to access the flattened elements for a given dimension. @@ -110,8 +110,9 @@ public interface IReadOnlyTensor : IReadOnlyTensor /// Attempts to copy the contents of this tensor into a destination tensor span and returns a value to indicate whether or not the operation succeeded. /// The target of the copy operation. /// if the copy operation succeeded; otherwise, false. + /// The source and overlap in an unsupported layout. /// - /// If the source and overlap, the entirety of the source is handled as if it was copied to a temporary location before it is copied to . + /// Overlapping dense tensors with equal element counts and identical views are supported. Other overlapping layouts throw before copying. /// If the length is shorter than the source, no items are copied and the method returns false. /// bool TryCopyTo(scoped in TensorSpan destination); @@ -119,8 +120,9 @@ public interface IReadOnlyTensor : IReadOnlyTensor /// Attempts to flatten the contents of this tensor into a destination span and returns a value to indicate whether or not the operation succeeded. /// The target of the copy operation. /// if the copy operation succeeded; otherwise, false. + /// The source and overlap in an unsupported layout. /// - /// If the source and overlap, the entirety of the source is handled as if it was flattened to a temporary location before it is copied to . + /// Overlapping dense tensors are supported. Other overlapping layouts throw before copying. /// If the length is shorter than the source, no items are copied and the method returns false. /// bool TryFlattenTo(scoped Span destination); diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/ReadOnlyTensorDimensionSpan_1.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/ReadOnlyTensorDimensionSpan_1.cs index 6aec1923473bf1..0ee034dfcd9e79 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/ReadOnlyTensorDimensionSpan_1.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/ReadOnlyTensorDimensionSpan_1.cs @@ -25,7 +25,7 @@ internal ReadOnlyTensorDimensionSpan(ReadOnlyTensorSpan tensor, int dimension dimension += 1; _tensor = tensor; - _length = TensorPrimitives.Product(tensor.Lengths[..dimension]); + _length = TensorShape.GetProduct(tensor.Lengths[..dimension]); _dimension = dimension; _sliceShape = TensorShape.Create((dimension != tensor.Rank) ? tensor.Lengths[dimension..] : [1], tensor.Strides[dimension..], tensor.IsPinned); } diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/ReadOnlyTensorSpan_1.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/ReadOnlyTensorSpan_1.cs index 10da93d71b51cf..f6193006e52602 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/ReadOnlyTensorSpan_1.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/ReadOnlyTensorSpan_1.cs @@ -148,8 +148,11 @@ public ReadOnlyTensorSpan(ReadOnlySpan span, scoped ReadOnlySpan length /// Returns default when is null. /// The created tensor span has a single dimension that is the same length as . /// + /// The type of is not compatible with an array of . public ReadOnlyTensorSpan(Array? array) { + ThrowHelper.ThrowIfArrayTypeMismatch(array, isReadOnly: true); + _shape = TensorShape.Create(array); _reference = ref (array is not null) ? ref Unsafe.As(ref MemoryMarshal.GetArrayDataReference(array)) @@ -175,8 +178,11 @@ public ReadOnlyTensorSpan(Array? array) /// * is not empty and contains an element that is negative. /// * is not empty and contains an element that is zero in a non leading position. /// + /// The type of is not compatible with an array of . public ReadOnlyTensorSpan(Array? array, scoped ReadOnlySpan start, scoped ReadOnlySpan lengths, scoped ReadOnlySpan strides) { + ThrowHelper.ThrowIfArrayTypeMismatch(array, isReadOnly: true); + _shape = TensorShape.Create(array, start, lengths, strides, out nint linearOffset); _reference = ref (array is not null) ? ref Unsafe.Add(ref Unsafe.As(ref MemoryMarshal.GetArrayDataReference(array)), linearOffset) @@ -402,7 +408,7 @@ public ReadOnlySpan GetSpan(scoped ReadOnlySpan startIndexes, int len /// public ReadOnlyTensorSpan Slice(params scoped ReadOnlySpan startIndexes) { - TensorShape shape = _shape.Slice(startIndexes, out nint linearOffset); + TensorShape shape = _shape.Slice(startIndexes, out nint linearOffset); return new ReadOnlyTensorSpan( ref Unsafe.Add(ref _reference, linearOffset), shape @@ -447,7 +453,8 @@ ref Unsafe.Add(ref _reference, linearOffset), /// public bool TryCopyTo(scoped in TensorSpan destination) { - if (TensorShape.AreCompatible(destination._shape, _shape, false)) + if ((_shape.FlattenedLength <= destination.FlattenedLength) && + TensorShape.AreCompatible(destination._shape, _shape, false)) { TensorOperation.Invoke, T, T>(this, destination); return true; @@ -536,46 +543,55 @@ ReadOnlyTensorSpan IReadOnlyTensor, T>.ToDenseTensor() public ref struct Enumerator : IEnumerator { private readonly ReadOnlyTensorSpan _span; - private readonly nint[] _indexes; private nint _linearOffset; private nint _itemsEnumerated; + private bool _hasCurrent; internal Enumerator(ReadOnlyTensorSpan span) { _span = span; - _indexes = new nint[span.Rank]; - - _indexes[^1] = -1; - - _linearOffset = 0 - (!span.IsEmpty ? span.Strides[^1] : 0); + _linearOffset = 0; _itemsEnumerated = 0; + _hasCurrent = false; } /// Gets the element at the current position of the enumerator. - public readonly ref readonly T Current => ref Unsafe.Add(ref _span._reference, _linearOffset); + public readonly ref readonly T Current + { + get + { + if (!_hasCurrent) + { + ThrowHelper.ThrowInvalidOperation_EnumerationNotPositioned(); + } + return ref Unsafe.Add(ref _span._reference, _linearOffset); + } + } /// Advances the enumerator to the next element of the tensor span. public bool MoveNext() { if (_itemsEnumerated == _span._shape.FlattenedLength) { + _hasCurrent = false; return false; } - _linearOffset = _span._shape.AdjustToNextIndex(_span._shape, _linearOffset, _indexes); + _linearOffset = _span.IsDense + ? _itemsEnumerated + : _span._shape.GetLinearOffsetForDimension(_itemsEnumerated, _span.Rank); _itemsEnumerated++; + _hasCurrent = true; return true; } /// Sets the enumerator to its initial position, which is before the first element in the tensor span. public void Reset() { - Array.Clear(_indexes); - _indexes[^1] = -1; - - _linearOffset = 0 - (!_span.IsEmpty ? _span.Strides[^1] : 0); + _linearOffset = 0; _itemsEnumerated = 0; + _hasCurrent = false; } // diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs index f99cef57ab903a..735a55734f6f74 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -81,8 +82,7 @@ public static Tensor Broadcast(scoped in ReadOnlyTensorSpan source, sco /// public static void BroadcastTo(this Tensor source, in TensorSpan destination) { - TensorOperation.ValidateCompatibility(source, destination); - TensorOperation.Invoke, T, T>(source, destination); + BroadcastTo(source.AsReadOnlyTensorSpan(), destination); } /// @@ -92,8 +92,7 @@ public static void BroadcastTo(this Tensor source, in TensorSpan destin /// Other to make shapes broadcastable. public static void BroadcastTo(in this TensorSpan source, in TensorSpan destination) { - TensorOperation.ValidateCompatibility(source, destination); - TensorOperation.Invoke, T, T>(source, destination); + BroadcastTo(source.AsReadOnlyTensorSpan(), destination); } /// @@ -126,10 +125,14 @@ public static Tensor Concatenate(params scoped ReadOnlySpan> ten public static Tensor ConcatenateOnDimension(int dimension, params scoped ReadOnlySpan> tensors) { if (tensors.Length < 2) + { ThrowHelper.ThrowArgument_ConcatenateTooFewTensors(); + } - if (dimension < -1 || dimension > tensors[0].Rank) + if (dimension < -1 || dimension >= tensors[0].Rank) + { ThrowHelper.ThrowArgument_InvalidDimension(); + } Tensor tensor; @@ -155,10 +158,11 @@ public static Tensor ConcatenateOnDimension(int dimension, params scoped R } } - nint[] lengths = new nint[tensors[0].Rank]; + scoped Span lengths = TensorOperation.RentedBuffer.CreateUninitialized(tensors[0].Rank, out TensorOperation.RentedBuffer lengthsRentedBuffer); tensors[0].Lengths.CopyTo(lengths); lengths[dimension] = sumOfAxis; tensor = CreateFromShape(lengths); + lengthsRentedBuffer.Dispose(); } else { @@ -203,18 +207,11 @@ public static ref readonly TensorSpan ConcatenateOnDimension(int dimension ThrowHelper.ThrowArgument_ConcatenateTooFewTensors(); } - if (dimension < -1 || dimension > tensors[0].Rank) + if (dimension < -1 || dimension >= tensors[0].Rank) { ThrowHelper.ThrowArgument_InvalidDimension(); } - // Calculate total space needed. - nint totalLength = 0; - for (int i = 0; i < tensors.Length; i++) - { - totalLength += tensors[i].FlattenedLength; - } - // If axis != -1, make sure all dimensions except the one to concatenate on match. if (dimension != -1) { @@ -236,35 +233,78 @@ public static ref readonly TensorSpan ConcatenateOnDimension(int dimension } } } - sumOfAxis += tensors[i].Lengths[dimension]; + sumOfAxis = checked(sumOfAxis + tensors[i].Lengths[dimension]); } // Make sure the destination tensor has the correct shape. - nint[] lengths = new nint[rank]; - tensors[0].Lengths.CopyTo(lengths); - lengths[dimension] = sumOfAxis; + if (destination.Rank != rank || destination.Lengths[dimension] != sumOfAxis) + { + ThrowHelper.ThrowArgument_DimensionsNotSame(nameof(destination)); + } + for (int i = 0; i < rank; i++) + { + if (i != dimension && destination.Lengths[i] != tensors[0].Lengths[i]) + { + ThrowHelper.ThrowArgument_DimensionsNotSame(nameof(destination)); + } + } + } + else + { + nint totalLength = 0; + for (int i = 0; i < tensors.Length; i++) + { + totalLength = checked(totalLength + tensors[i].FlattenedLength); + } - if (!TensorShape.AreLengthsTheSame(destination.Lengths, lengths)) + if (destination.FlattenedLength != totalLength) { ThrowHelper.ThrowArgument_DimensionsNotSame(nameof(destination)); } } + if (destination._shape.IsSelfOverlapping) + { + ThrowHelper.ThrowArgument_DestinationHasOverlappingElements(nameof(destination)); + } + + for (int i = 0; i < tensors.Length; i++) + { + ReadOnlyTensorSpan source = tensors[i].AsReadOnlyTensorSpan(); + if (source._shape.Overlaps(in source._reference, ref destination._reference, destination._shape.LinearLength)) + { + ThrowHelper.ThrowArgument_OverlappingTensorLayoutsNotSupported(); + } + } + if (!destination.IsDense) { - // For non-dense destinations, concatenate into a temporary dense buffer, - // then copy element-by-element to respect the destination's stride layout. - T[] tempBuffer = ArrayPool.Shared.Rent((int)totalLength); - try + if (dimension == -1) { - Span tempSpan = tempBuffer.AsSpan(0, (int)totalLength); - ConcatenateOnDimensionToSpan(dimension, tensors, destination, tempSpan); - ReadOnlyTensorSpan tempTensor = new ReadOnlyTensorSpan(tempBuffer, 0, destination.Lengths, []); - TensorOperation.Invoke, T, T>(tempTensor, destination); + TensorSpan.Enumerator output = destination.GetEnumerator(); + foreach (Tensor tensor in tensors) + { + Tensor.Enumerator input = tensor.GetEnumerator(); + while (input.MoveNext()) + { + bool moved = output.MoveNext(); + Debug.Assert(moved); + output.Current = input.Current; + } + } } - finally + else { - ArrayPool.Shared.Return(tempBuffer); + Span ranges = TensorOperation.RentedBuffer.CreateUninitialized(destination.Rank, out TensorOperation.RentedBuffer rentedBuffer); + ranges.Fill(NRange.All); + nint offset = 0; + foreach (Tensor tensor in tensors) + { + ranges[dimension] = new NRange(offset, offset + tensor.Lengths[dimension]); + TensorOperation.Invoke, T, T>(tensor.AsReadOnlyTensorSpan(), destination.Slice(ranges)); + offset += tensor.Lengths[dimension]; + } + rentedBuffer.Dispose(); } } else @@ -298,12 +338,12 @@ private static void ConcatenateOnDimensionToSpan(int dimension, scoped ReadOn ranges[i] = ..; } - bool hasMore = true; + bool hasMore = !destination.Lengths[..dimension].Contains(0); while (hasMore) { for (int i = 0; i < tensors.Length; i++) { - Tensor slice = tensors[i].Slice(ranges); + ReadOnlyTensorSpan slice = tensors[i].AsReadOnlyTensorSpan().Slice(ranges); TensorOperation.Invoke, T, T>(slice, dstSpan); dstSpan = dstSpan.Slice((int)slice.FlattenedLength); } @@ -1322,14 +1362,16 @@ public static bool LessThanOrEqualAny(T x, in ReadOnlyTensorSpan y) /// with the new axis ordering. public static Tensor PermuteDimensions(this Tensor tensor, ReadOnlySpan dimensions) { - if (tensor.Rank == 1) + if (tensor.Rank <= 1 && (dimensions.IsEmpty || (dimensions.Length == 1 && dimensions[0] == 0))) { return tensor; } else { if (!dimensions.IsEmpty && dimensions.Length != tensor.Lengths.Length) + { ThrowHelper.ThrowArgument_PermuteAxisOrder(); + } scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer lengthsRentedBuffer); scoped Span newStrides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer stridesRentedBuffer); @@ -1346,17 +1388,30 @@ public static Tensor PermuteDimensions(this Tensor tensor, ReadOnlySpan } else { + scoped Span seen = TensorOperation.RentedBuffer.CreateUninitialized( + (tensor.Rank - 1) / 64 + 1, out TensorOperation.RentedBuffer seenRentedBuffer); for (int i = 0; i < dimensions.Length; i++) { - if (dimensions[i] >= tensor.Lengths.Length || dimensions[i] < 0) + int dimension = dimensions[i]; + if ((uint)dimension >= (uint)tensor.Lengths.Length) { ThrowHelper.ThrowArgument_InvalidDimension(); } - newLengths[i] = tensor.Lengths[dimensions[i]]; - newStrides[i] = tensor.Strides[dimensions[i]]; + + int word = dimension / 64; + ulong bit = 1UL << (dimension % 64); + if ((seen[word] & bit) != 0) + { + ThrowHelper.ThrowArgument_PermuteAxisOrder(); + } + + seen[word] |= bit; + newLengths[i] = tensor.Lengths[dimension]; + newStrides[i] = tensor.Strides[dimension]; } + seenRentedBuffer.Dispose(); } - outTensor = new Tensor(tensor._values, tensor._start, newLengths, newStrides); + outTensor = new Tensor(tensor._values, tensor._start, newLengths, newStrides, tensor.IsPinned); lengthsRentedBuffer.Dispose(); stridesRentedBuffer.Dispose(); @@ -1367,77 +1422,98 @@ public static Tensor PermuteDimensions(this Tensor tensor, ReadOnlySpan #endregion #region Reshape - /// - /// Reshapes the tensor to the specified . If one of the lengths is -1, it will be calculated automatically. - /// Does not change the length of the underlying memory nor does it allocate new memory. If the new shape is not compatible with the old shape, - /// an exception is thrown. - /// - /// you want to reshape. - /// with the new dimensions. - public static Tensor Reshape(this Tensor tensor, ReadOnlySpan lengths) + private static void GetReshapeLengths(ReadOnlySpan lengths, nint flattenedLength, Span result) { - if (tensor.Lengths.SequenceEqual(lengths)) - return tensor; - - if (!tensor.IsDense && !tensor.Strides.Contains(0)) + if (lengths.IsEmpty) { - ThrowHelper.ThrowArgument_CannotReshapeNonContiguousOrDense(); + if (flattenedLength != 0) + { + ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); + } + return; } - nint[] newLengths = [.. lengths]; - // Calculate wildcard info. - int wildcardIndex = lengths.IndexOf(-1); - if (wildcardIndex >= 0) + lengths.CopyTo(result); + int wildcardIndex = -1; + nint knownProduct = 1; + for (int i = 0; i < lengths.Length; i++) { - if (lengths.Count(-1) > 1) - ThrowHelper.ThrowArgument_OnlyOneWildcard(); - nint tempTotal = tensor.FlattenedLength; - for (int i = 0; i < lengths.Length; i++) + nint length = lengths[i]; + if (length == -1) { - if (lengths[i] != -1) + if (wildcardIndex >= 0) { - tempTotal /= lengths[i]; + ThrowHelper.ThrowArgument_OnlyOneWildcard(); } + wildcardIndex = i; + } + else + { + if (length < 0 || (length != 0 && knownProduct > nint.MaxValue / length)) + { + ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); + } + knownProduct *= length; } - newLengths[wildcardIndex] = tempTotal; } - nint tempLinear = TensorPrimitives.Product(newLengths); - if (tempLinear != tensor.FlattenedLength) + if (wildcardIndex >= 0) + { + if (knownProduct == 0 || flattenedLength % knownProduct != 0) + { + ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); + } + result[wildcardIndex] = flattenedLength / knownProduct; + } + else if (knownProduct != flattenedLength) + { ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); + } + + } - nint[] strides; + private static Span GetReshapeStrides(ReadOnlySpan sourceLengths, ReadOnlySpan sourceStrides, + bool isDense, ReadOnlySpan lengths, [UnscopedRef] out TensorOperation.RentedBuffer rentedBuffer) + { + if (isDense) + { + rentedBuffer = default; + return []; + } - // If all our strides are 0 we can reshape however we like and keep all new strides at 0 - if (!tensor.Strides.ContainsAnyExcept(0)) + Span strides = TensorOperation.RentedBuffer.CreateUninitialized(lengths.Length, out rentedBuffer); + if (!sourceStrides.ContainsAnyExcept(0)) { - strides = new nint[newLengths.Length]; + return strides; } - // If we contain a 0 stride we can only add dimensions of length 1. - else if (tensor.Strides.Contains(0)) + + int sourceIndex = 0; + for (int i = 0; i < lengths.Length; i++) { - List origStrides = [.. tensor.Strides]; - int lengthOffset = 0; - for (int i = 0; i < newLengths.Length; i++) + while (sourceIndex < sourceLengths.Length && sourceLengths[sourceIndex] == 1 && lengths[i] != 1) { - if (lengthOffset < tensor.Rank && newLengths[i] == tensor.Lengths[lengthOffset]) - lengthOffset++; - else if (newLengths[i] == 1) - { - if (lengthOffset == tensor.Rank) - origStrides.Add(tensor.Strides[lengthOffset - 1]); - else - origStrides.Insert(i, tensor.Strides[i] * tensor.Lengths[i]); - } - else - ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); + sourceIndex++; + } + + if (sourceIndex < sourceLengths.Length && lengths[i] == sourceLengths[sourceIndex]) + { + strides[i] = sourceStrides[sourceIndex++]; + } + else if (lengths[i] != 1) + { + ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); } - strides = [.. origStrides]; } - else - strides = []; - return new Tensor(tensor._values, tensor._start, newLengths, strides); + while (sourceIndex < sourceLengths.Length && sourceLengths[sourceIndex] == 1) + { + sourceIndex++; + } + if (sourceIndex != sourceLengths.Length) + { + ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); + } + return strides; } /// @@ -1445,74 +1521,56 @@ public static Tensor Reshape(this Tensor tensor, ReadOnlySpan len /// Does not change the length of the underlying memory nor does it allocate new memory. If the new shape is not compatible with the old shape, /// an exception is thrown. /// - /// you want to reshape. + /// you want to reshape. /// with the new dimensions. - public static TensorSpan Reshape(this scoped in TensorSpan tensor, scoped ReadOnlySpan lengths) + public static Tensor Reshape(this Tensor tensor, ReadOnlySpan lengths) { if (tensor.Lengths.SequenceEqual(lengths)) + { return tensor; + } if (!tensor.IsDense && !tensor.Strides.Contains(0)) { ThrowHelper.ThrowArgument_CannotReshapeNonContiguousOrDense(); } - nint[] newLengths = [.. lengths]; - int wildcardIndex = lengths.IndexOf(-1); - if (wildcardIndex >= 0) - { - if (lengths.Count(-1) > 1) - ThrowHelper.ThrowArgument_OnlyOneWildcard(); - nint tempTotal = tensor.FlattenedLength; - for (int i = 0; i < lengths.Length; i++) - { - if (lengths[i] != -1) - { - tempTotal /= lengths[i]; - } - } - newLengths[wildcardIndex] = tempTotal; - - } + scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(lengths.Length, out TensorOperation.RentedBuffer lengthsRentedBuffer); + GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); + scoped Span strides = GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, out TensorOperation.RentedBuffer stridesRentedBuffer); - nint tempLinear = TensorPrimitives.Product(newLengths); - if (tempLinear != tensor.FlattenedLength) - ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); - - nint[] strides; + Tensor result = new Tensor(tensor._values, tensor._start, newLengths, strides, tensor.IsPinned); + stridesRentedBuffer.Dispose(); + lengthsRentedBuffer.Dispose(); + return result; + } - // If all our strides are 0 we can reshape however we like and keep all new strides at 0 - if (!tensor.Strides.ContainsAnyExcept(0)) + /// + /// Reshapes the tensor to the specified . If one of the lengths is -1, it will be calculated automatically. + /// Does not change the length of the underlying memory nor does it allocate new memory. If the new shape is not compatible with the old shape, + /// an exception is thrown. + /// + /// you want to reshape. + /// with the new dimensions. + public static TensorSpan Reshape(this scoped in TensorSpan tensor, scoped ReadOnlySpan lengths) + { + if (tensor.Lengths.SequenceEqual(lengths)) { - strides = new nint[newLengths.Length]; + return tensor; } - // If we contain a 0 stride we can only add dimensions of length 1. - else if (tensor.Strides.Contains(0)) + + if (!tensor.IsDense && !tensor.Strides.Contains(0)) { - List origStrides = [.. tensor.Strides]; - int lengthOffset = 0; - for (int i = 0; i < newLengths.Length; i++) - { - if (lengthOffset < tensor.Rank && newLengths[i] == tensor.Lengths[lengthOffset]) - { - lengthOffset++; - } - else if (newLengths[i] == 1) - { - if (lengthOffset == tensor.Rank) - origStrides.Add(tensor.Strides[lengthOffset - 1]); - else - origStrides.Insert(i, tensor.Strides[i] * tensor.Lengths[i]); - } - else - ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); - } - strides = [.. origStrides]; + ThrowHelper.ThrowArgument_CannotReshapeNonContiguousOrDense(); } - else - strides = []; + + scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(lengths.Length, out TensorOperation.RentedBuffer lengthsRentedBuffer); + GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); + scoped Span strides = GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, out TensorOperation.RentedBuffer stridesRentedBuffer); TensorSpan output = new TensorSpan(ref tensor._reference, tensor._shape.LinearLength, newLengths, strides, tensor.IsPinned); + stridesRentedBuffer.Dispose(); + lengthsRentedBuffer.Dispose(); return output; } @@ -1526,68 +1584,22 @@ public static TensorSpan Reshape(this scoped in TensorSpan tensor, scop public static ReadOnlyTensorSpan Reshape(this scoped in ReadOnlyTensorSpan tensor, scoped ReadOnlySpan lengths) { if (tensor.Lengths.SequenceEqual(lengths)) + { return tensor; + } if (!tensor.IsDense && !tensor.Strides.Contains(0)) { ThrowHelper.ThrowArgument_CannotReshapeNonContiguousOrDense(); } - nint[] newLengths = [.. lengths]; - // Calculate wildcard info. - int wildcardIndex = lengths.IndexOf(-1); - if (wildcardIndex >= 0) - { - if (lengths.Count(-1) > 1) - ThrowHelper.ThrowArgument_OnlyOneWildcard(); - nint tempTotal = tensor.FlattenedLength; - for (int i = 0; i < lengths.Length; i++) - { - if (lengths[i] != -1) - { - tempTotal /= lengths[i]; - } - } - newLengths[wildcardIndex] = tempTotal; - - } - - nint tempLinear = TensorPrimitives.Product(newLengths); - if (tempLinear != tensor.FlattenedLength) - ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); - - nint[] strides; - - // If all our strides are 0 we can reshape however we like and keep all new strides at 0 - if (!tensor.Strides.ContainsAnyExcept(0)) - { - strides = new nint[newLengths.Length]; - } - // If we contain a 0 stride we can only add dimensions of length 1. - else if (tensor.Strides.Contains(0)) - { - List origStrides = [.. tensor.Strides]; - int lengthOffset = 0; - for (int i = 0; i < newLengths.Length; i++) - { - if (lengthOffset < tensor.Rank && newLengths[i] == tensor.Lengths[lengthOffset]) - lengthOffset++; - else if (newLengths[i] == 1) - { - if (lengthOffset == tensor.Rank) - origStrides.Add(tensor.Strides[lengthOffset - 1]); - else - origStrides.Insert(i, tensor.Strides[i] * tensor.Lengths[i]); - } - else - ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); - } - strides = [.. origStrides]; - } - else - strides = []; + scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(lengths.Length, out TensorOperation.RentedBuffer lengthsRentedBuffer); + GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); + scoped Span strides = GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, out TensorOperation.RentedBuffer stridesRentedBuffer); ReadOnlyTensorSpan output = new ReadOnlyTensorSpan(ref tensor._reference, tensor._shape.LinearLength, newLengths, strides, tensor.IsPinned); + stridesRentedBuffer.Dispose(); + lengthsRentedBuffer.Dispose(); return output; } #endregion @@ -1601,9 +1613,8 @@ public static ReadOnlyTensorSpan Reshape(this scoped in ReadOnlyTensorSpan /// of the desired new shape. public static Tensor Resize(Tensor tensor, ReadOnlySpan lengths) { - nint newSize = TensorPrimitives.Product(lengths); - T[] values = tensor.IsPinned ? GC.AllocateArray((int)newSize) : (new T[newSize]); - Tensor output = Create(values, lengths, []); + Tensor output = CreateFromShape(lengths, tensor.IsPinned); + nint newSize = output.FlattenedLength; if (tensor.IsDense) { @@ -1637,7 +1648,7 @@ public static Tensor Resize(Tensor tensor, ReadOnlySpan lengths) /// /// Copies the data from . If the final shape is smaller all data after that point is ignored. - /// If the final shape is bigger it is filled with 0s. + /// If the final shape is bigger it is filled with the default value of . /// /// Input . /// Destination with the desired new shape. @@ -1648,7 +1659,7 @@ public static void ResizeTo(scoped in Tensor tensor, in TensorSpan dest /// /// Copies the data from . If the final shape is smaller all data after that point is ignored. - /// If the final shape is bigger it is filled with 0s. + /// If the final shape is bigger it is filled with the default value of . /// /// Input . /// Destination with the desired new shape. @@ -1659,12 +1670,17 @@ public static void ResizeTo(scoped in TensorSpan tensor, in TensorSpan /// /// Copies the data from . If the final shape is smaller all data after that point is ignored. - /// If the final shape is bigger it is filled with 0s. + /// If the final shape is bigger it is filled with the default value of . /// /// Input . /// Destination with the desired new shape. public static void ResizeTo(scoped in ReadOnlyTensorSpan tensor, in TensorSpan destination) { + if (destination._shape.IsSelfOverlapping) + { + ThrowHelper.ThrowArgument_DestinationHasOverlappingElements(nameof(destination)); + } + if (tensor.IsDense && destination.IsDense) { ReadOnlySpan span = MemoryMarshal.CreateSpan(ref tensor._reference, (int)tensor.FlattenedLength); @@ -1672,6 +1688,7 @@ public static void ResizeTo(scoped in ReadOnlyTensorSpan tensor, in Tensor if (ospan.Length >= span.Length) { span.CopyTo(ospan); + ospan[span.Length..].Clear(); } else { @@ -1681,6 +1698,12 @@ public static void ResizeTo(scoped in ReadOnlyTensorSpan tensor, in Tensor else { nint copyLength = Math.Min(tensor.FlattenedLength, destination.FlattenedLength); + + if (tensor._shape.Overlaps(in tensor._reference, ref destination._reference, destination._shape.LinearLength)) + { + ThrowHelper.ThrowArgument_OverlappingTensorLayoutsNotSupported(); + } + ReadOnlyTensorSpan.Enumerator srcEnumerator = tensor.GetEnumerator(); TensorSpan.Enumerator dstEnumerator = destination.GetEnumerator(); @@ -1691,6 +1714,13 @@ public static void ResizeTo(scoped in ReadOnlyTensorSpan tensor, in Tensor Debug.Assert(srcMoved && dstMoved); dstEnumerator.Current = srcEnumerator.Current; } + + for (nint i = copyLength; i < destination.FlattenedLength; i++) + { + bool dstMoved = dstEnumerator.MoveNext(); + Debug.Assert(dstMoved); + dstEnumerator.Current = default!; + } } } #endregion @@ -1741,36 +1771,103 @@ public static ref readonly TensorSpan Reverse(scoped in ReadOnlyTensorSpan /// dimension along which to reverse over. -1 will reverse over all of the dimensions of the left tensor. public static ref readonly TensorSpan ReverseDimension(scoped in ReadOnlyTensorSpan tensor, in TensorSpan destination, int dimension) { - // When the dimension is -1, its just a straight reverse copy. + if (dimension < -1 || dimension >= tensor.Rank) + { + ThrowHelper.ThrowArgument_InvalidDimension(); + } + TensorOperation.ValidateCompatibility(tensor, destination); + if (tensor.FlattenedLength == 0) + { + return ref destination; + } + if (tensor._shape.Overlaps(in tensor._reference, ref destination._reference, destination._shape.LinearLength)) + { + if (Unsafe.AreSame(ref Unsafe.AsRef(in tensor._reference), ref destination._reference) + && tensor.Lengths.SequenceEqual(destination.Lengths) + && tensor.Strides.SequenceEqual(destination.Strides)) + { + if ((tensor._shape.LinearLength == 1) || (dimension >= 0 && tensor.Lengths[dimension] == 1)) + { + return ref destination; + } + if (tensor.IsDense) + { + ReverseDimensionInPlace(destination, dimension); + return ref destination; + } + } + ThrowHelper.ThrowArgument_OverlappingTensorLayoutsNotSupported(); + } + ReverseDimensionCore(tensor, destination, dimension); + + return ref destination; + } + + private static void ReverseDimensionInPlace(in TensorSpan tensor, int dimension) + { + nint axisLength = dimension == -1 ? tensor.FlattenedLength : tensor.Lengths[dimension]; + nint innerLength = 1; + if (dimension >= 0) + { + for (int i = dimension + 1; i < tensor.Rank; i++) + { + innerLength *= tensor.Lengths[i]; + } + } + + nint blockLength = axisLength * innerLength; + for (nint block = 0; block < tensor.FlattenedLength; block += blockLength) + { + for (nint left = 0; left < axisLength / 2; left++) + { + nint leftOffset = block + left * innerLength; + nint rightOffset = block + (axisLength - 1 - left) * innerLength; + for (nint i = 0; i < innerLength; i++) + { + ref T first = ref Unsafe.Add(ref tensor._reference, leftOffset + i); + ref T second = ref Unsafe.Add(ref tensor._reference, rightOffset + i); + (first, second) = (second, first); + } + } + } + } + + private static void ReverseDimensionCore(scoped in ReadOnlyTensorSpan tensor, in TensorSpan destination, int dimension) + { if (dimension == -1) { - TensorOperation.ValidateCompatibility(tensor, destination); TensorOperation.ReverseInvoke, T, T>(tensor, destination); } - // With any other dimension, we need to copy the data in reverse order based on the provided dimension. + else if (tensor.Lengths[dimension] == 1) + { + TensorOperation.Invoke, T, T>(tensor, destination); + } else { - TensorOperation.ValidateCompatibility(tensor, destination); Span srcIndexes = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer srcIndexesRentedBuffer); - Span dstIndexes = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer dstIndexesRentedBuffer); + Span dstIndexes = TensorOperation.RentedBuffer.CreateUninitialized(destination.Rank, out TensorOperation.RentedBuffer dstIndexesRentedBuffer); for (int i = 0; i < srcIndexes.Length; i++) { srcIndexes[i] = NRange.All; + } + for (int i = 0; i < dstIndexes.Length; i++) + { dstIndexes[i] = NRange.All; } - for (int i = (int)tensor.Lengths[dimension]; i > 0; i--) + int destinationAxis = dimension + destination.Rank - tensor.Rank; + for (nint i = destination.Lengths[destinationAxis]; i > 0; i--) { - srcIndexes[dimension] = new NRange(i - 1, i); - dstIndexes[dimension] = new NRange(tensor.Lengths[dimension] - i, tensor.Lengths[dimension] - i + 1); + nint sourceIndex = i - 1; + srcIndexes[dimension] = new NRange(sourceIndex, sourceIndex + 1); + dstIndexes[destinationAxis] = new NRange(destination.Lengths[destinationAxis] - i, destination.Lengths[destinationAxis] - i + 1); TensorOperation.Invoke, T, T>(tensor.Slice(srcIndexes), destination.Slice(dstIndexes)); } srcIndexesRentedBuffer.Dispose(); dstIndexesRentedBuffer.Dispose(); } - return ref destination; } #endregion @@ -1871,11 +1968,12 @@ public static Tensor[] Split(scoped in ReadOnlyTensorSpan tensor, int s nint totalToCopy = tensor.FlattenedLength / splitCount; - nint[] newLengths = [.. tensor.Lengths]; - nint splitLength = newLengths[dimension] / splitCount; - newLengths[dimension] = splitLength; + scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer lengthsRentedBuffer); + tensor.Lengths.CopyTo(newLengths); + nint splitLength = newLengths[(int)dimension] / splitCount; + newLengths[(int)dimension] = splitLength; - scoped Span sliceDims = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer lengthsRentedBuffer); + scoped Span sliceDims = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer sliceDimsRentedBuffer); for (int i = 0; i < sliceDims.Length; i++) { sliceDims[i] = NRange.All; @@ -1891,6 +1989,7 @@ public static Tensor[] Split(scoped in ReadOnlyTensorSpan tensor, int s start += splitLength; } + sliceDimsRentedBuffer.Dispose(); lengthsRentedBuffer.Dispose(); return outputs; } @@ -1916,6 +2015,11 @@ public static Tensor SqueezeDimension(this Tensor tensor, int dimension { int rank = tensor.Rank; + if (rank == 0 && dimension == -1) + { + return tensor; + } + if ((dimension >= rank) || (dimension < -1)) { ThrowHelper.ThrowArgument_AxisLargerThanRank(); @@ -1956,7 +2060,14 @@ public static Tensor SqueezeDimension(this Tensor tensor, int dimension tensor.Strides[(dimension + 1)..].CopyTo(strides[dimension..]); } - Tensor output = new Tensor(tensor._values, tensor._start, lengths[..rank], strides[..rank]); + if (rank == 0) + { + lengths[0] = 1; + strides[0] = 0; + rank = 1; + } + + Tensor output = new Tensor(tensor._values, tensor._start, lengths[..rank], strides[..rank], tensor.IsPinned); lengthsRentedBuffer.Dispose(); stridesRentedBuffer.Dispose(); @@ -1983,6 +2094,11 @@ public static TensorSpan SqueezeDimension(this scoped in TensorSpan ten { int rank = tensor.Rank; + if (rank == 0 && dimension == -1) + { + return tensor; + } + if ((dimension >= rank) || (dimension < -1)) { ThrowHelper.ThrowArgument_AxisLargerThanRank(); @@ -2023,6 +2139,13 @@ public static TensorSpan SqueezeDimension(this scoped in TensorSpan ten tensor.Strides[(dimension + 1)..].CopyTo(strides[dimension..]); } + if (rank == 0) + { + lengths[0] = 1; + strides[0] = 0; + rank = 1; + } + TensorSpan output = new TensorSpan(ref tensor._reference, tensor._shape.LinearLength, lengths[..rank], strides[..rank], tensor.IsPinned); lengthsRentedBuffer.Dispose(); @@ -2050,6 +2173,11 @@ public static ReadOnlyTensorSpan SqueezeDimension(this scoped in ReadOnlyT { int rank = tensor.Rank; + if (rank == 0 && dimension == -1) + { + return tensor; + } + if ((dimension >= rank) || (dimension < -1)) { ThrowHelper.ThrowArgument_AxisLargerThanRank(); @@ -2090,6 +2218,13 @@ public static ReadOnlyTensorSpan SqueezeDimension(this scoped in ReadOnlyT tensor.Strides[(dimension + 1)..].CopyTo(strides[dimension..]); } + if (rank == 0) + { + lengths[0] = 1; + strides[0] = 0; + rank = 1; + } + ReadOnlyTensorSpan output = new ReadOnlyTensorSpan(ref tensor._reference, tensor._shape.LinearLength, lengths[..rank], strides[..rank], tensor.IsPinned); lengthsRentedBuffer.Dispose(); @@ -2185,6 +2320,10 @@ internal static string ToString(in ReadOnlyTensorSpan tensor, ReadOnlySpan if (!maximumLengths.IsEmpty) { ArgumentOutOfRangeException.ThrowIfNotEqual(maximumLengths.Length, tensor.Rank); + foreach (nint maximumLength in maximumLengths) + { + ArgumentOutOfRangeException.ThrowIfNegative(maximumLength, nameof(maximumLengths)); + } } var sb = new StringBuilder(typeName); @@ -2282,7 +2421,7 @@ private static void ToString(in ReadOnlyTensorSpan tensor, nint length, St for (nint i = 1; i < length; i++) { sb.Append(", "); - sb.Append(Unsafe.Add(ref tensor._reference, i)); + sb.Append(Unsafe.Add(ref tensor._reference, i * tensor.Strides[0])); } if (length != tensor.Lengths[0]) @@ -2347,7 +2486,7 @@ public static Tensor Transpose(Tensor tensor) strides[^1] = strides[^2]; strides[^2] = temp; - Tensor output = new Tensor(tensor._values, tensor._start, lengths, strides); + Tensor output = new Tensor(tensor._values, tensor._start, lengths, strides, tensor.IsPinned); lengthsRentedBuffer.Dispose(); stridesRentedBuffer.Dispose(); @@ -2387,9 +2526,10 @@ public static bool TryBroadcastTo(in this TensorSpan tensor, in TensorSpan /// Destination . public static bool TryBroadcastTo(in this ReadOnlyTensorSpan tensor, in TensorSpan destination) { - TensorOperation.ValidateCompatibility(tensor, destination); if (!TensorShape.AreCompatible(destination._shape, tensor._shape, false)) + { return false; + } BroadcastTo(tensor, destination); return true; @@ -2428,7 +2568,7 @@ public static Tensor Unsqueeze(this Tensor tensor, int dimension) newStrides[dimension] = 0; } - Tensor output = new Tensor(tensor._values, tensor._start, newLengths, newStrides); + Tensor output = new Tensor(tensor._values, tensor._start, newLengths, newStrides, tensor.IsPinned); lengthsRentedBuffer.Dispose(); stridesRentedBuffer.Dispose(); return output; @@ -2735,7 +2875,7 @@ public static ref readonly TensorSpan Atan(scoped in ReadOnlyTensorSpan public static Tensor Atan2(in ReadOnlyTensorSpan x, in ReadOnlyTensorSpan y) where T : IFloatingPointIeee754 { - Tensor destination = CreateFromShapeUninitialized(x.Lengths); + TensorOperation.ValidateCompatibility(x, y, out Tensor destination); TensorOperation.Invoke, T, T>(x, y, destination); return destination; } @@ -2790,7 +2930,7 @@ public static Tensor Atan2(T x, in ReadOnlyTensorSpan y) where T : IFloatingPointIeee754 { Tensor destination = CreateFromShapeUninitialized(y.Lengths); - TensorOperation.Invoke, T, T>(y, x, destination); + TensorOperation.Invoke, T, T>(x, y, destination); return destination; } @@ -2804,7 +2944,7 @@ public static ref readonly TensorSpan Atan2(T x, scoped in ReadOnlyTensorS where T : IFloatingPointIeee754 { TensorOperation.ValidateCompatibility(y, destination); - TensorOperation.Invoke, T, T>(y, x, destination); + TensorOperation.Invoke, T, T>(x, y, destination); return ref destination; } #endregion @@ -2818,7 +2958,7 @@ public static ref readonly TensorSpan Atan2(T x, scoped in ReadOnlyTensorS public static Tensor Atan2Pi(in ReadOnlyTensorSpan x, in ReadOnlyTensorSpan y) where T : IFloatingPointIeee754 { - Tensor destination = CreateFromShapeUninitialized(x.Lengths); + TensorOperation.ValidateCompatibility(x, y, out Tensor destination); TensorOperation.Invoke, T, T>(x, y, destination); return destination; } @@ -2873,7 +3013,7 @@ public static Tensor Atan2Pi(T x, in ReadOnlyTensorSpan y) where T : IFloatingPointIeee754 { Tensor destination = CreateFromShapeUninitialized(y.Lengths); - TensorOperation.Invoke, T, T>(y, x, destination); + TensorOperation.Invoke, T, T>(x, y, destination); return destination; } @@ -2887,7 +3027,7 @@ public static ref readonly TensorSpan Atan2Pi(T x, scoped in ReadOnlyTenso where T : IFloatingPointIeee754 { TensorOperation.ValidateCompatibility(y, destination); - TensorOperation.Invoke, T, T>(y, x, destination); + TensorOperation.Invoke, T, T>(x, y, destination); return ref destination; } #endregion @@ -3132,7 +3272,7 @@ public static Tensor CopySign(in ReadOnlyTensorSpan x, T sign) public static Tensor CopySign(in ReadOnlyTensorSpan x, in ReadOnlyTensorSpan sign) where T : INumber { - Tensor destination = CreateFromShapeUninitialized(x.Lengths); + TensorOperation.ValidateCompatibility(x, sign, out Tensor destination); TensorOperation.Invoke, T, T>(x, sign, destination); return destination; } @@ -3657,15 +3797,10 @@ private static nint IndexOfMaxFallback(scoped in ReadOnlyTensorSpan x) where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - try - { - x.FlattenTo(flat); - return TensorPrimitives.IndexOfMax(flat.AsSpan(0, (int)x.FlattenedLength)); - } - finally - { - ArrayPool.Shared.Return(flat); - } + x.FlattenTo(flat); + nint result = TensorPrimitives.IndexOfMax(flat.AsSpan(0, (int)x.FlattenedLength)); + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + return result; } #endregion @@ -3689,15 +3824,10 @@ private static nint IndexOfMaxMagnitudeFallback(scoped in ReadOnlyTensorSpan< where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - try - { - x.FlattenTo(flat); - return TensorPrimitives.IndexOfMaxMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); - } - finally - { - ArrayPool.Shared.Return(flat); - } + x.FlattenTo(flat); + nint result = TensorPrimitives.IndexOfMaxMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + return result; } #endregion @@ -3720,15 +3850,10 @@ private static nint IndexOfMinFallback(scoped in ReadOnlyTensorSpan x) where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - try - { - x.FlattenTo(flat); - return TensorPrimitives.IndexOfMin(flat.AsSpan(0, (int)x.FlattenedLength)); - } - finally - { - ArrayPool.Shared.Return(flat); - } + x.FlattenTo(flat); + nint result = TensorPrimitives.IndexOfMin(flat.AsSpan(0, (int)x.FlattenedLength)); + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + return result; } #endregion @@ -3753,15 +3878,10 @@ private static nint IndexOfMinMagnitudeFallback(scoped in ReadOnlyTensorSpan< where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - try - { - x.FlattenTo(flat); - return TensorPrimitives.IndexOfMinMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); - } - finally - { - ArrayPool.Shared.Return(flat); - } + x.FlattenTo(flat); + nint result = TensorPrimitives.IndexOfMinMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + return result; } #endregion @@ -4945,11 +5065,11 @@ public static ref readonly TensorSpan SinPi(scoped in ReadOnlyTensorSpan SoftMax(in ReadOnlyTensorSpan x) where T : IExponentialFunctions { - T sumExp = T.AdditiveIdentity; - TensorOperation.Invoke, T, T>(x, ref sumExp); + (T Shift, T Sum, bool HasValue) sumExp = default; + TensorOperation.Invoke, T, (T Shift, T Sum, bool HasValue)>(x, ref sumExp); Tensor destination = CreateFromShapeUninitialized(x.Lengths); - TensorOperation.Invoke, T, T>(x, sumExp, destination); + TensorOperation.Invoke, T, (T Shift, T Sum), T>(x, (sumExp.Shift, sumExp.Sum), destination); return destination; } @@ -4959,11 +5079,10 @@ public static Tensor SoftMax(in ReadOnlyTensorSpan x) public static ref readonly TensorSpan SoftMax(scoped in ReadOnlyTensorSpan x, in TensorSpan destination) where T : IExponentialFunctions { - T sumExp = T.AdditiveIdentity; - TensorOperation.Invoke, T, T>(x, ref sumExp); - TensorOperation.ValidateCompatibility(x, destination); - TensorOperation.Invoke, T, T>(x, sumExp, destination); + (T Shift, T Sum, bool HasValue) sumExp = default; + TensorOperation.Invoke, T, (T Shift, T Sum, bool HasValue)>(x, ref sumExp); + TensorOperation.Invoke, T, (T Shift, T Sum), T>(x, (sumExp.Shift, sumExp.Sum), destination); return ref destination; } #endregion diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_BitwiseAnd.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_BitwiseAnd.cs index dc2417aeb7db48..e886d606c6c720 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_BitwiseAnd.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_BitwiseAnd.cs @@ -14,7 +14,7 @@ public static partial class Tensor public static Tensor BitwiseAnd(in ReadOnlyTensorSpan x, in ReadOnlyTensorSpan y) where T : IBitwiseOperators { - Tensor destination = CreateFromShapeUninitialized(x.Lengths); + TensorOperation.ValidateCompatibility(x, y, out Tensor destination); TensorOperation.Invoke, T, T>(x, y, destination); return destination; } diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_BitwiseOr.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_BitwiseOr.cs index f54f3600adeb12..aa0b88422fd8e1 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_BitwiseOr.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_BitwiseOr.cs @@ -14,7 +14,7 @@ public static partial class Tensor public static Tensor BitwiseOr(in ReadOnlyTensorSpan x, in ReadOnlyTensorSpan y) where T : IBitwiseOperators { - Tensor destination = CreateFromShapeUninitialized(x.Lengths); + TensorOperation.ValidateCompatibility(x, y, out Tensor destination); TensorOperation.Invoke, T, T>(x, y, destination); return destination; } diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_ExclusiveOr.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_ExclusiveOr.cs index 867873be53aa24..5b3a84e9ec7aa3 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_ExclusiveOr.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.op_ExclusiveOr.cs @@ -14,7 +14,7 @@ public static partial class Tensor public static Tensor Xor(in ReadOnlyTensorSpan x, in ReadOnlyTensorSpan y) where T : IBitwiseOperators { - Tensor destination = CreateFromShapeUninitialized(x.Lengths); + TensorOperation.ValidateCompatibility(x, y, out Tensor destination); TensorOperation.Invoke, T, T>(x, y, destination); return destination; } diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorDimensionSpan_1.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorDimensionSpan_1.cs index 7aae9d2706c02b..1958f2967867b6 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorDimensionSpan_1.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorDimensionSpan_1.cs @@ -25,7 +25,7 @@ internal TensorDimensionSpan(TensorSpan tensor, int dimension) dimension += 1; _tensor = tensor; - _length = TensorPrimitives.Product(tensor.Lengths[..dimension]); + _length = TensorShape.GetProduct(tensor.Lengths[..dimension]); _dimension = dimension; _sliceShape = TensorShape.Create((dimension != tensor.Rank) ? tensor.Lengths[dimension..] : [1], tensor.Strides[dimension..], tensor.IsPinned); } @@ -57,7 +57,7 @@ public TensorSpan this[nint index] /// The tensor dimension span to convert to a readonly tensor dimension span. /// The tensor dimension span that corresponds to . public static implicit operator ReadOnlyTensorDimensionSpan(scoped in TensorDimensionSpan tensorDimension) => - new ReadOnlyTensorDimensionSpan(tensorDimension._tensor, tensorDimension._dimension); + new ReadOnlyTensorDimensionSpan(tensorDimension._tensor, tensorDimension._dimension - 1); /// Gets an enumerator for the readonly tensor dimension span. public Enumerator GetEnumerator() => new Enumerator(this); diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs index d02135740cfcb9..66128355f65c27 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs @@ -5,11 +5,46 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace System.Numerics.Tensors { internal static class TensorOperation { + private static bool ValidateSourceOverlap( + in ReadOnlyTensorSpan source, in TensorSpan destination, bool isCopy = false) + { + if (source.FlattenedLength == 0 || destination.FlattenedLength == 0) + { + return false; + } + + if (!source._shape.Overlaps(in source._reference, ref destination._reference, destination._shape.LinearLength)) + { + return false; + } + + if (typeof(TSource) == typeof(TDestination)) + { + ref TSource destinationReference = ref Unsafe.As(ref destination._reference); + if (Unsafe.AreSame(ref Unsafe.AsRef(in source._reference), ref destinationReference) + && source.Lengths.SequenceEqual(destination.Lengths) + && source.Strides.SequenceEqual(destination.Strides)) + { + if (isCopy) + { + return true; + } + if (!source._shape.IsSelfOverlapping) + { + return false; + } + } + } + + throw new ArgumentException(SR.Argument_OverlappingTensorLayoutsNotSupported, nameof(destination)); + } + public static void Invoke(in TensorSpan x) where TOperation : IOperation { @@ -31,7 +66,8 @@ public static bool Invoke(in ReadOnlyTensorSpan x, in Re { bool result = false; - ref readonly TensorShape destinationShape = ref ((x._shape.FlattenedLength > y._shape.FlattenedLength) ? ref x._shape : ref y._shape); + TensorShape destinationShape = GetBroadcastShape(x._shape, y._shape); + scoped Span xIndexes = RentedBuffer.Create(destinationShape.Rank, x.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); scoped Span yIndexes = RentedBuffer.Create(destinationShape.Rank, y.Strides, out nint yLinearOffset, out RentedBuffer yRentedBuffer); @@ -55,7 +91,30 @@ ref result xRentedBuffer.Dispose(); yRentedBuffer.Dispose(); - return result; + return (destinationShape.FlattenedLength == 0) || result; + } + + private static TensorShape GetBroadcastShape(in TensorShape x, in TensorShape y) + { + if (TensorShape.AreLengthsTheSame(x, y)) + { + return x; + } + + int rank = Math.Max(x.Rank, y.Rank); + scoped Span lengths = RentedBuffer.CreateUninitialized(rank, out RentedBuffer rentedBuffer); + ReadOnlySpan xLengths = x.Lengths; + ReadOnlySpan yLengths = y.Lengths; + for (int i = 0; i < rank; i++) + { + nint xLength = i < rank - x.Rank ? 1 : xLengths[i - (rank - x.Rank)]; + nint yLength = i < rank - y.Rank ? 1 : yLengths[i - (rank - y.Rank)]; + lengths[i] = xLength == 1 ? yLength : xLength; + } + + TensorShape shape = TensorShape.Create(lengths, [], pinned: false); + rentedBuffer.Dispose(); + return shape; } public static bool Invoke(in ReadOnlyTensorSpan x, TArg y) @@ -77,13 +136,12 @@ ref result if (!result) { - return false; + break; } } xRentedBuffer.Dispose(); - - return result; + return (x.FlattenedLength == 0) || result; } public static void Invoke(in TensorSpan destination, TArg scalar) @@ -106,22 +164,39 @@ ref Unsafe.Add(ref destination._reference, linearOffset), public static void Invoke(in ReadOnlyTensorSpan x, in TensorSpan destination) where TOperation : IUnaryOperation_Tensor { - scoped Span xIndexes = RentedBuffer.Create(destination.Rank, x.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); - scoped Span destinationIndexes = RentedBuffer.Create(destination.Rank, destination.Strides, out nint destinationLinearOffset, out RentedBuffer destinationRentedBuffer); - - for (nint i = 0; i < destination.FlattenedLength; i++) + bool isCopy = typeof(TOperation) == typeof(CopyTo) && typeof(TArg) == typeof(TResult); + if (isCopy) { - xLinearOffset = x._shape.AdjustToNextIndex(destination._shape, xLinearOffset, xIndexes); - destinationLinearOffset = destination._shape.AdjustToNextIndex(destination._shape, destinationLinearOffset, destinationIndexes); + ref TArg destinationReference = ref Unsafe.As(ref destination._reference); + if (x.IsDense && destination.IsDense && x.FlattenedLength == destination.FlattenedLength && x.FlattenedLength <= int.MaxValue) + { + MemoryMarshal.CreateReadOnlySpan(in x._reference, (int)x.FlattenedLength) + .CopyTo(MemoryMarshal.CreateSpan(ref destinationReference, (int)destination.FlattenedLength)); + return; + } + } - TOperation.Invoke( - in Unsafe.Add(ref x._reference, xLinearOffset), - ref Unsafe.Add(ref destination._reference, destinationLinearOffset) - ); + if (ValidateSourceOverlap(x, destination, isCopy)) + { + return; } + { + ReadOnlyTensorSpan source = x; + scoped Span xIndexes = RentedBuffer.Create(destination.Rank, source.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); + scoped Span destinationIndexes = RentedBuffer.Create(destination.Rank, destination.Strides, out nint destinationLinearOffset, out RentedBuffer destinationRentedBuffer); + for (nint i = 0; i < destination.FlattenedLength; i++) + { + xLinearOffset = source._shape.AdjustToNextIndex(destination._shape, xLinearOffset, xIndexes); + destinationLinearOffset = destination._shape.AdjustToNextIndex(destination._shape, destinationLinearOffset, destinationIndexes); - xRentedBuffer.Dispose(); - destinationRentedBuffer.Dispose(); + TOperation.Invoke( + in Unsafe.Add(ref source._reference, xLinearOffset), + ref Unsafe.Add(ref destination._reference, destinationLinearOffset) + ); + } + xRentedBuffer.Dispose(); + destinationRentedBuffer.Dispose(); + } } public static void ReverseInvoke(in ReadOnlyTensorSpan x, in TensorSpan destination) @@ -156,17 +231,34 @@ ref Unsafe.Add(ref destination._reference, destinationLinearOffset) public static void Invoke(in ReadOnlyTensorSpan x, in Span destination) where TOperation : IUnaryOperation_Tensor { + Span result = destination[..checked((int)x.FlattenedLength)]; + if (typeof(TOperation) == typeof(CopyTo) && typeof(TArg) == typeof(TResult)) + { + ref TArg destinationReference = ref Unsafe.As(ref MemoryMarshal.GetReference(result)); + if (x.IsDense) + { + MemoryMarshal.CreateReadOnlySpan(in x._reference, result.Length) + .CopyTo(MemoryMarshal.CreateSpan(ref destinationReference, result.Length)); + return; + } + + if (x._shape.Overlaps(in x._reference, ref destinationReference, result.Length)) + { + ThrowHelper.ThrowArgument_OverlappingTensorLayoutsNotSupported(); + } + } + scoped Span xIndexes = RentedBuffer.Create(x.Rank, x.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); nint destinationIndex = -1; - for (nint i = 0; i < destination.Length; i++) + for (nint i = 0; i < result.Length; i++) { xLinearOffset = x._shape.AdjustToNextIndex(x._shape, xLinearOffset, xIndexes); destinationIndex++; TOperation.Invoke( in Unsafe.Add(ref x._reference, xLinearOffset), - ref Unsafe.Add(ref destination[0], destinationIndex) + ref Unsafe.Add(ref result[0], destinationIndex) ); } @@ -194,26 +286,30 @@ ref destination public static void Invoke(in ReadOnlyTensorSpan x, in ReadOnlyTensorSpan y, in TensorSpan destination) where TOperation : IBinaryOperation_Tensor_Tensor { - scoped Span xIndexes = RentedBuffer.Create(destination.Rank, x.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); - scoped Span yIndexes = RentedBuffer.Create(destination.Rank, y.Strides, out nint yLinearOffset, out RentedBuffer yRentedBuffer); - scoped Span destinationIndexes = RentedBuffer.Create(destination.Rank, destination.Strides, out nint destinationLinearOffset, out RentedBuffer destinationRentedBuffer); - - for (nint i = 0; i < destination.FlattenedLength; i++) + ValidateSourceOverlap(x, destination); + ValidateSourceOverlap(y, destination); { - xLinearOffset = x._shape.AdjustToNextIndex(destination._shape, xLinearOffset, xIndexes); - yLinearOffset = y._shape.AdjustToNextIndex(destination._shape, yLinearOffset, yIndexes); - destinationLinearOffset = destination._shape.AdjustToNextIndex(destination._shape, destinationLinearOffset, destinationIndexes); - - TOperation.Invoke( - in Unsafe.Add(ref x._reference, xLinearOffset), - in Unsafe.Add(ref y._reference, yLinearOffset), - ref Unsafe.Add(ref destination._reference, destinationLinearOffset) - ); + ReadOnlyTensorSpan left = x; + ReadOnlyTensorSpan right = y; + scoped Span xIndexes = RentedBuffer.Create(destination.Rank, left.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); + scoped Span yIndexes = RentedBuffer.Create(destination.Rank, right.Strides, out nint yLinearOffset, out RentedBuffer yRentedBuffer); + scoped Span destinationIndexes = RentedBuffer.Create(destination.Rank, destination.Strides, out nint destinationLinearOffset, out RentedBuffer destinationRentedBuffer); + for (nint i = 0; i < destination.FlattenedLength; i++) + { + xLinearOffset = left._shape.AdjustToNextIndex(destination._shape, xLinearOffset, xIndexes); + yLinearOffset = right._shape.AdjustToNextIndex(destination._shape, yLinearOffset, yIndexes); + destinationLinearOffset = destination._shape.AdjustToNextIndex(destination._shape, destinationLinearOffset, destinationIndexes); + + TOperation.Invoke( + in Unsafe.Add(ref left._reference, xLinearOffset), + in Unsafe.Add(ref right._reference, yLinearOffset), + ref Unsafe.Add(ref destination._reference, destinationLinearOffset) + ); + } + xRentedBuffer.Dispose(); + yRentedBuffer.Dispose(); + destinationRentedBuffer.Dispose(); } - - xRentedBuffer.Dispose(); - yRentedBuffer.Dispose(); - destinationRentedBuffer.Dispose(); } public static void Invoke(in ReadOnlyTensorSpan x, in ReadOnlyTensorSpan y, in TensorSpan destination) @@ -223,7 +319,7 @@ public static void Invoke(in ReadOnlyTensorSpan public static void Invoke(in ReadOnlyTensorSpan x, in ReadOnlyTensorSpan y, ref TResult result) where TOperation : IBinaryOperation_Tensor_Tensor { - ref readonly TensorShape destinationShape = ref ((x._shape.FlattenedLength > y._shape.FlattenedLength) ? ref x._shape : ref y._shape); + TensorShape destinationShape = GetBroadcastShape(x._shape, y._shape); scoped Span xIndexes = RentedBuffer.Create(destinationShape.Rank, x.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); scoped Span yIndexes = RentedBuffer.Create(destinationShape.Rank, y.Strides, out nint yLinearOffset, out RentedBuffer yRentedBuffer); @@ -255,45 +351,49 @@ public static void Invoke(in ReadOnlyTensorSpan public static void Invoke(in ReadOnlyTensorSpan x, TArg2 y, in TensorSpan destination) where TOperation : IBinaryOperation_Tensor_Scalar { - scoped Span xIndexes = RentedBuffer.Create(destination.Rank, x.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); - scoped Span destinationIndexes = RentedBuffer.Create(destination.Rank, destination.Strides, out nint destinationLinearOffset, out RentedBuffer destinationRentedBuffer); - - for (nint i = 0; i < destination.FlattenedLength; i++) + ValidateSourceOverlap(x, destination); { - xLinearOffset = x._shape.AdjustToNextIndex(destination._shape, xLinearOffset, xIndexes); - destinationLinearOffset = destination._shape.AdjustToNextIndex(destination._shape, destinationLinearOffset, destinationIndexes); - - TOperation.Invoke( - in Unsafe.Add(ref x._reference, xLinearOffset), - y, - ref Unsafe.Add(ref destination._reference, destinationLinearOffset) - ); + ReadOnlyTensorSpan source = x; + scoped Span xIndexes = RentedBuffer.Create(destination.Rank, source.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); + scoped Span destinationIndexes = RentedBuffer.Create(destination.Rank, destination.Strides, out nint destinationLinearOffset, out RentedBuffer destinationRentedBuffer); + for (nint i = 0; i < destination.FlattenedLength; i++) + { + xLinearOffset = source._shape.AdjustToNextIndex(destination._shape, xLinearOffset, xIndexes); + destinationLinearOffset = destination._shape.AdjustToNextIndex(destination._shape, destinationLinearOffset, destinationIndexes); + + TOperation.Invoke( + in Unsafe.Add(ref source._reference, xLinearOffset), + y, + ref Unsafe.Add(ref destination._reference, destinationLinearOffset) + ); + } + xRentedBuffer.Dispose(); + destinationRentedBuffer.Dispose(); } - - xRentedBuffer.Dispose(); - destinationRentedBuffer.Dispose(); } public static void Invoke(TArg x, in ReadOnlyTensorSpan y, in TensorSpan destination) where TOperation : IBinaryOperation_Scalar_Tensor { - scoped Span xIndexes = RentedBuffer.Create(destination.Rank, y.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); - scoped Span destinationIndexes = RentedBuffer.Create(destination.Rank, destination.Strides, out nint destinationLinearOffset, out RentedBuffer destinationRentedBuffer); - - for (nint i = 0; i < destination.FlattenedLength; i++) + ValidateSourceOverlap(y, destination); { - xLinearOffset = y._shape.AdjustToNextIndex(destination._shape, xLinearOffset, xIndexes); - destinationLinearOffset = destination._shape.AdjustToNextIndex(destination._shape, destinationLinearOffset, destinationIndexes); - - TOperation.Invoke( - x, - in Unsafe.Add(ref y._reference, xLinearOffset), - ref Unsafe.Add(ref destination._reference, destinationLinearOffset) - ); + ReadOnlyTensorSpan source = y; + scoped Span xIndexes = RentedBuffer.Create(destination.Rank, source.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); + scoped Span destinationIndexes = RentedBuffer.Create(destination.Rank, destination.Strides, out nint destinationLinearOffset, out RentedBuffer destinationRentedBuffer); + for (nint i = 0; i < destination.FlattenedLength; i++) + { + xLinearOffset = source._shape.AdjustToNextIndex(destination._shape, xLinearOffset, xIndexes); + destinationLinearOffset = destination._shape.AdjustToNextIndex(destination._shape, destinationLinearOffset, destinationIndexes); + + TOperation.Invoke( + x, + in Unsafe.Add(ref source._reference, xLinearOffset), + ref Unsafe.Add(ref destination._reference, destinationLinearOffset) + ); + } + xRentedBuffer.Dispose(); + destinationRentedBuffer.Dispose(); } - - xRentedBuffer.Dispose(); - destinationRentedBuffer.Dispose(); } public static void Invoke(in ReadOnlyTensorSpan x, TArg2 y, ref TResult result) @@ -363,11 +463,7 @@ public static void ValidateCompatibility(in ReadOnlyTensorSpan resultLengths = (maxRank <= TensorShape.MaxInlineRank) - ? stackalloc nint[TensorShape.MaxInlineRank] - : (resultLengthsArray = ArrayPool.Shared.Rent(maxRank)); - resultLengths = resultLengths[..maxRank]; + scoped Span resultLengths = RentedBuffer.CreateUninitialized(maxRank, out RentedBuffer resultLengthsRentedBuffer); ReadOnlySpan xLengths = x.Lengths; ReadOnlySpan yLengths = y.Lengths; @@ -379,15 +475,12 @@ public static void ValidateCompatibility(in ReadOnlyTensorSpan= xOffset) ? xLengths[i - xOffset] : 1; nint yLen = (i >= yOffset) ? yLengths[i - yOffset] : 1; - resultLengths[i] = Math.Max(xLen, yLen); + resultLengths[i] = (xLen == 1) ? yLen : xLen; } destination = Tensor.CreateFromShapeUninitialized(resultLengths); - if (resultLengthsArray is not null) - { - ArrayPool.Shared.Return(resultLengthsArray); - } + resultLengthsRentedBuffer.Dispose(); return; } @@ -660,7 +753,8 @@ public static void Invoke(ReadOnlySpan x, Span destination) } public readonly struct Atan2 - : IBinaryOperation_Tensor_Scalar, + : IBinaryOperation_Scalar_Tensor, + IBinaryOperation_Tensor_Scalar, IBinaryOperation_Tensor_Tensor where T : IFloatingPointIeee754 { @@ -686,10 +780,24 @@ public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destinat { TensorPrimitives.Atan2(x, y, destination); } + + public static void Invoke(T x, ref readonly T y, ref T destination) + { + destination = T.Atan2(x, y); + } + + public static void Invoke(T x, ReadOnlySpan y, Span destination) + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = T.Atan2(x, y[i]); + } + } } public readonly struct Atan2Pi - : IBinaryOperation_Tensor_Scalar, + : IBinaryOperation_Scalar_Tensor, + IBinaryOperation_Tensor_Scalar, IBinaryOperation_Tensor_Tensor where T : IFloatingPointIeee754 { @@ -715,6 +823,19 @@ public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destinat { TensorPrimitives.Atan2Pi(x, y, destination); } + + public static void Invoke(T x, ref readonly T y, ref T destination) + { + destination = T.Atan2Pi(x, y); + } + + public static void Invoke(T x, ReadOnlySpan y, Span destination) + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = T.Atan2Pi(x, y[i]); + } + } } public readonly struct Atanh @@ -1420,7 +1541,7 @@ public static void Invoke(ReadOnlySpan x, ref T destination) public static void Invoke(ref readonly T x, ref readonly T y, ref T destination) { - destination = T.MaxMagnitude(x, destination); + destination = T.MaxMagnitude(x, y); } public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destination) @@ -1430,7 +1551,7 @@ public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destinat public static void Invoke(ref readonly T x, T y, ref T destination) { - destination = T.MaxMagnitude(x, destination); + destination = T.MaxMagnitude(x, y); } public static void Invoke(ReadOnlySpan x, T y, Span destination) @@ -1457,7 +1578,7 @@ public static void Invoke(ReadOnlySpan x, ref T destination) public static void Invoke(ref readonly T x, ref readonly T y, ref T destination) { - destination = T.MaxMagnitudeNumber(x, destination); + destination = T.MaxMagnitudeNumber(x, y); } public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destination) @@ -1467,7 +1588,7 @@ public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destinat public static void Invoke(ref readonly T x, T y, ref T destination) { - destination = T.MaxMagnitudeNumber(x, destination); + destination = T.MaxMagnitudeNumber(x, y); } public static void Invoke(ReadOnlySpan x, T y, Span destination) @@ -1494,7 +1615,7 @@ public static void Invoke(ReadOnlySpan x, ref T destination) public static void Invoke(ref readonly T x, ref readonly T y, ref T destination) { - destination = T.MaxNumber(x, destination); + destination = T.MaxNumber(x, y); } public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destination) @@ -1504,7 +1625,7 @@ public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destinat public static void Invoke(ref readonly T x, T y, ref T destination) { - destination = T.MaxNumber(x, destination); + destination = T.MaxNumber(x, y); } public static void Invoke(ReadOnlySpan x, T y, Span destination) @@ -1568,7 +1689,7 @@ public static void Invoke(ReadOnlySpan x, ref T destination) public static void Invoke(ref readonly T x, ref readonly T y, ref T destination) { - destination = T.MinMagnitude(x, destination); + destination = T.MinMagnitude(x, y); } public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destination) @@ -1578,7 +1699,7 @@ public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destinat public static void Invoke(ref readonly T x, T y, ref T destination) { - destination = T.MinMagnitude(x, destination); + destination = T.MinMagnitude(x, y); } public static void Invoke(ReadOnlySpan x, T y, Span destination) @@ -1605,7 +1726,7 @@ public static void Invoke(ReadOnlySpan x, ref T destination) public static void Invoke(ref readonly T x, ref readonly T y, ref T destination) { - destination = T.MinMagnitudeNumber(x, destination); + destination = T.MinMagnitudeNumber(x, y); } public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destination) @@ -1615,7 +1736,7 @@ public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destinat public static void Invoke(ref readonly T x, T y, ref T destination) { - destination = T.MinMagnitudeNumber(x, destination); + destination = T.MinMagnitudeNumber(x, y); } public static void Invoke(ReadOnlySpan x, T y, Span destination) @@ -1642,7 +1763,7 @@ public static void Invoke(ReadOnlySpan x, ref T destination) public static void Invoke(ref readonly T x, ref readonly T y, ref T destination) { - destination = T.MinNumber(x, destination); + destination = T.MinNumber(x, y); } public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destination) @@ -1652,7 +1773,7 @@ public static void Invoke(ReadOnlySpan x, ReadOnlySpan y, Span destinat public static void Invoke(ref readonly T x, T y, ref T destination) { - destination = T.MinNumber(x, destination); + destination = T.MinNumber(x, y); } public static void Invoke(ReadOnlySpan x, T y, Span destination) @@ -1990,37 +2111,71 @@ public static void Invoke(ReadOnlySpan x, Span destination) // SoftMax Helper public readonly struct SumExp - : IUnaryReduction_Tensor + : IUnaryReduction_Tensor where T : IExponentialFunctions { - public static void Invoke(ref readonly T x, ref T destination) + public static void Invoke(ref readonly T x, ref (T Shift, T Sum, bool HasValue) destination) { - destination += T.Exp(x); + if (!destination.HasValue) + { + destination = (x, T.One, true); + return; + } + + if (T.IsNegativeInfinity(x)) + { + return; + } + + T term = T.Exp(x - destination.Shift); + if (T.IsInfinity(term)) + { + destination.Sum = (destination.Sum * T.Exp(destination.Shift - x)) + T.One; + destination.Shift = x; + } + else if (T.IsInfinity(destination.Sum + term)) + { + T previousShift = destination.Shift; + T step = T.One; + T nextShift = previousShift + step; + while (nextShift.Equals(previousShift)) + { + step += step; + nextShift = previousShift + step; + } + T scale = T.Exp(previousShift - nextShift); + destination.Shift = nextShift; + destination.Sum = (destination.Sum * scale) + (term * scale); + } + else + { + destination.Sum += term; + } } - public static void Invoke(ReadOnlySpan x, ref T destination) + public static void Invoke(ReadOnlySpan x, ref (T Shift, T Sum, bool HasValue) destination) { for (int i = 0; i < x.Length; i++) { - destination += T.Exp(x[i]); + Invoke(in x[i], ref destination); } } } public readonly struct SoftMax - : IBinaryOperation_Tensor_Scalar + : IBinaryOperation_Tensor_Scalar where T : IExponentialFunctions { - public static void Invoke(ref readonly T x, T y, ref T destination) + public static void Invoke(ref readonly T x, (T Shift, T Sum) y, ref T destination) { - destination = T.Exp(x) / y; + destination = T.Exp(x - y.Shift) / y.Sum; } - public static void Invoke(ReadOnlySpan x, T y, Span destination) + public static void Invoke(ReadOnlySpan x, (T Shift, T Sum) y, Span destination) { for (int i = 0; i < x.Length; i++) { - destination[i] = T.Exp(x[i]) / y; + destination[i] = T.Exp(x[i] - y.Shift) / y.Sum; } } } @@ -2738,7 +2893,10 @@ public static Span Create(int rank, ReadOnlySpan strides, out nint l Span output = RentedBuffer.Create(rank, out rentedBuffer); linearOffset = 0 - (!strides.IsEmpty ? strides[^1] : 0); - output[^1] = T.CreateChecked(-1); + if (!output.IsEmpty) + { + output[^1] = T.CreateChecked(-1); + } return output; } diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorShape.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorShape.cs index 577656f536c156..1a46c7d75644e4 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorShape.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorShape.cs @@ -119,7 +119,7 @@ private TensorShape(nint linearLength, scoped ReadOnlySpan lengths, scoped if (length > 1) { - maximumLinearIndex = checked(maximumLinearIndex + ((length - 1) * stride)); + maximumLinearIndex = CheckedShapeAdd(maximumLinearIndex, CheckedShapeMultiply(length - 1, stride)); } else { @@ -128,7 +128,7 @@ private TensorShape(nint linearLength, scoped ReadOnlySpan lengths, scoped } destinationStrides[i] = stride; - flattenedLength = checked(flattenedLength * length); + flattenedLength = CheckedShapeMultiply(flattenedLength, length); } // When the strides are automatically computed, then we must be dense @@ -156,7 +156,7 @@ private TensorShape(nint linearLength, scoped ReadOnlySpan lengths, scoped int maxStrideIndex = rank - 1; nint minimumNonZeroStride = 1; - int[]? stridesOrderArray; + int[]? stridesOrderArray = null; InlineBuffer stridesOrderBuffer; scoped Span stridesOrder; @@ -225,8 +225,8 @@ private TensorShape(nint linearLength, scoped ReadOnlySpan lengths, scoped ThrowHelper.ThrowArgument_InvalidTensorShape(); } - minimumNonZeroStride = checked(length * sortedStride); - maximumLinearIndex = checked(maximumLinearIndex + (minimumNonZeroStride - sortedStride)); + minimumNonZeroStride = CheckedShapeMultiply(length, sortedStride); + maximumLinearIndex = CheckedShapeAdd(maximumLinearIndex, minimumNonZeroStride - sortedStride); } else { @@ -239,7 +239,7 @@ private TensorShape(nint linearLength, scoped ReadOnlySpan lengths, scoped } } - flattenedLength = checked(flattenedLength * length); + flattenedLength = CheckedShapeMultiply(flattenedLength, length); } strides.CopyTo(destinationStrides); @@ -251,6 +251,11 @@ private TensorShape(nint linearLength, scoped ReadOnlySpan lengths, scoped { flags |= TensorFlags.HasAnyDenseDimensions; } + + if (stridesOrderArray is not null) + { + ArrayPool.Shared.Return(stridesOrderArray); + } } // Once we've finished computing everything physically present in the input @@ -272,10 +277,49 @@ private TensorShape(nint linearLength, scoped ReadOnlySpan lengths, scoped _rank = rank; _flags = flags; - ValidateState(); } + private static nint CheckedShapeMultiply(nint left, nint right) + { + try + { + return checked(left * right); + } + catch (OverflowException) + { + throw new ArgumentOutOfRangeException(); + } + } + + internal static nint GetProduct(ReadOnlySpan lengths) + { + if (lengths.Contains(0)) + { + return 0; + } + + nint product = 1; + foreach (nint length in lengths) + { + product = CheckedShapeMultiply(product, length); + } + + return product; + } + + private static nint CheckedShapeAdd(nint left, nint right) + { + try + { + return checked(left + right); + } + catch (OverflowException) + { + throw new ArgumentOutOfRangeException(); + } + } + private TensorShape(nint flattenedLength, nint linearLength, scoped ReadOnlySpan lengths, scoped ReadOnlySpan strides, TensorFlags flags) { int rank = lengths.Length; @@ -315,6 +359,29 @@ private TensorShape(nint flattenedLength, nint linearLength, scoped ReadOnlySpan public bool HasAnyDenseDimensions => (_flags & TensorFlags.HasAnyDenseDimensions) != 0; + internal bool IsSelfOverlapping + { + get + { + if (IsEmpty || IsDense) + { + return false; + } + + ReadOnlySpan lengths = Lengths; + ReadOnlySpan strides = Strides; + for (int i = 0; i < lengths.Length; i++) + { + if (lengths[i] > 1 && strides[i] == 0) + { + return true; + } + } + + return false; + } + } + public bool IsBroadcast => (_flags & TensorFlags.IsBroadcast) != 0; public bool IsDense => (_flags & TensorFlags.IsDense) != 0; @@ -325,6 +392,22 @@ private TensorShape(nint flattenedLength, nint linearLength, scoped ReadOnlySpan public nint LinearLength => _linearLength; + internal bool Overlaps(ref readonly TSource source, ref TDestination destination, nint destinationLength) + { + if ((LinearLength == 0) || (destinationLength == 0)) + { + return false; + } + + nint byteOffset = Unsafe.ByteOffset(ref Unsafe.AsRef(in source), ref Unsafe.As(ref destination)); + nuint sourceBytes = checked((nuint)LinearLength * (nuint)Unsafe.SizeOf()); + nuint destinationBytes = checked((nuint)destinationLength * (nuint)Unsafe.SizeOf()); + + return byteOffset >= 0 + ? (nuint)byteOffset < sourceBytes + : unchecked((nuint)(0 - (nuint)byteOffset)) < destinationBytes; + } + [UnscopedRef] public ReadOnlySpan Lengths { @@ -576,15 +659,9 @@ public static bool AreCompatible(in TensorShape shape1, in TensorShape shape2, b Debug.Assert(rankDelta > 0); } - // We need both to be empty if either is empty - - if (shape1.IsEmpty) + if ((shape1.Rank == 0) || (shape2.Rank == 0)) { - return shape2.IsEmpty; - } - else if (shape2.IsEmpty) - { - return false; + return shape1.IsEmpty && shape2.IsEmpty; } // We need the lengths to be equal, length2 to be 1, or @@ -614,7 +691,7 @@ public static bool AreCompatible(in TensorShape shape1, in TensorShape shape2, b return false; } - if (!allowBidirectional) + if (!allowBidirectional && !shape1.IsEmpty) { // When we aren't bidirectionally compatible, then we // need to ensure that if stride1 is 0, then stride2 @@ -647,13 +724,11 @@ public static bool AreCompatible(in ReadOnlySpan shape1Lengths, in TensorS return false; } - // We need both to be empty if either is empty - if (shape1Lengths.IsEmpty) { return shape2.IsEmpty; } - else if (shape2.IsEmpty) + if (shape2.Rank == 0) { return false; } @@ -874,11 +949,11 @@ public static TensorShape Create(T[]? array) return default; } - public static TensorShape Create(T[]? array, scoped ReadOnlySpan lengths, scoped ReadOnlySpan strides) + public static TensorShape Create(T[]? array, scoped ReadOnlySpan lengths, scoped ReadOnlySpan strides, bool pinned = false) { if (array is not null) { - return new TensorShape(array.Length, lengths, strides, TensorFlags.None); + return new TensorShape(array.Length, lengths, strides, pinned ? TensorFlags.IsPinned : TensorFlags.None); } if ((lengths.Length != 0) || (strides.Length != 0)) @@ -888,7 +963,7 @@ public static TensorShape Create(T[]? array, scoped ReadOnlySpan length return default; } - public static TensorShape Create(T[]? array, int start, scoped ReadOnlySpan lengths, scoped ReadOnlySpan strides) + public static TensorShape Create(T[]? array, int start, scoped ReadOnlySpan lengths, scoped ReadOnlySpan strides, bool pinned = false) { if (array is not null) { @@ -900,7 +975,7 @@ public static TensorShape Create(T[]? array, int start, scoped ReadOnlySpan(T[]? array, int start, scoped ReadOnlySpan(ref readonly T reference, nint linearLength, bool pinned) { + ArgumentOutOfRangeException.ThrowIfNegative(linearLength); + if (!Unsafe.IsNullRef(in reference)) { nint stride = 1; @@ -946,6 +1023,8 @@ public static TensorShape Create(ref readonly T reference, nint linearLength, public static TensorShape Create(ref readonly T reference, nint linearLength, scoped ReadOnlySpan lengths, scoped ReadOnlySpan strides, bool pinned) { + ArgumentOutOfRangeException.ThrowIfNegative(linearLength); + if (!Unsafe.IsNullRef(in reference)) { TensorFlags flags = pinned ? TensorFlags.IsPinned : TensorFlags.None; @@ -1007,6 +1086,10 @@ public nint GetLinearOffset(ReadOnlySpan state) { ThrowHelper.ThrowArgumentOutOfRangeException(); } + if (IsEmpty) + { + ThrowHelper.ThrowIndexOutOfRangeException(); + } nint linearOffset = 0; @@ -1028,6 +1111,10 @@ public nint GetLinearOffsetForDimension(nint index, int dimension) { ThrowHelper.ThrowArgumentOutOfRangeException(); } + if (dimension == 0 || lengths[..dimension].Contains(0)) + { + ThrowHelper.ThrowIndexOutOfRangeException(); + } nint linearOffset = 0; @@ -1057,6 +1144,11 @@ public nint GetLongestContiguousLength(ReadOnlySpan s { ThrowHelper.ThrowArgumentOutOfRangeException(); } + if (rank == 0) + { + linearOffset = 0; + return 0; + } nint maximumLinearIndex = 0; nint minimumNonZeroStride = 1; @@ -1091,7 +1183,10 @@ public nint GetLongestContiguousLength(ReadOnlySpan s // We are no longer dense since we have a broadcast to more than 1 element longestContiguousLength = maximumLinearIndex + 1; } - minimumNonZeroStride = adjustedStride * length; + if (length > 1) + { + minimumNonZeroStride = adjustedStride * length; + } computedOffset += (offset * stride); } @@ -1109,6 +1204,16 @@ public TensorShape Slice(ReadOnlySpan state, out nint { int rank = Rank; + if (rank == 0) + { + if (!state.IsEmpty) + { + ThrowHelper.ThrowArgumentOutOfRangeException(); + } + linearOffset = 0; + return this; + } + nint[]? intermediateLengthsArray = null; InlineBuffer intermediateLengthsBuffer; scoped Span intermediateLengths; @@ -1192,7 +1297,10 @@ public TensorShape Slice(ReadOnlySpan state, out nint intermediateLengths[i] = length; intermediateStrides[i] = stride; - minimumNonZeroStride = stride * length; + if (length > 1) + { + minimumNonZeroStride = stride * length; + } computedOffset += (offset * previousStride); flattenedLength *= length; @@ -1231,7 +1339,10 @@ public TensorShape Slice(ReadOnlySpan state, out nint ArrayPool.Shared.Return(intermediateStridesArray); } - Debug.Assert(computedOffset == GetLinearOffset(state)); + if (!IsEmpty) + { + Debug.Assert(computedOffset == GetLinearOffset(state)); + } linearOffset = computedOffset; return result; @@ -1306,6 +1417,24 @@ public static (nint Offset, nint Length) GetOffsetAndLength(nint index, nint len } } + public readonly struct GetOffsetAndLengthForSlice : IGetOffsetAndLength + { + public static nint GetOffset(nint index, nint length) + { + if ((index < 0) || (index >= length)) + { + ThrowHelper.ThrowArgumentOutOfRangeException(); + } + return index; + } + + public static (nint Offset, nint Length) GetOffsetAndLength(nint index, nint length) + { + nint offset = GetOffset(index, length); + return (offset, length - offset); + } + } + public readonly struct GetOffsetAndLengthForNIndex : IGetOffsetAndLength { public static nint GetOffset(NIndex index, nint length) diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorSpan_1.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorSpan_1.cs index 908373f13980ed..8f5c5f3ac681e7 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorSpan_1.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorSpan_1.cs @@ -91,10 +91,10 @@ public TensorSpan(Span span, scoped ReadOnlySpan lengths, scoped ReadOn } /// - /// is covariant and its type is not exactly T[]. + /// is not compatible with an array of , or is covariant. public TensorSpan(Array? array) { - ThrowHelper.ThrowIfArrayTypeMismatch(array); + ThrowHelper.ThrowIfArrayTypeMismatch(array, isReadOnly: false); _shape = TensorShape.Create(array); _reference = ref (array is not null) @@ -103,10 +103,10 @@ public TensorSpan(Array? array) } /// - /// is covariant and its type is not exactly T[]. + /// is not compatible with an array of , or is covariant. public TensorSpan(Array? array, scoped ReadOnlySpan start, scoped ReadOnlySpan lengths, scoped ReadOnlySpan strides) { - ThrowHelper.ThrowIfArrayTypeMismatch(array); + ThrowHelper.ThrowIfArrayTypeMismatch(array, isReadOnly: false); _shape = TensorShape.Create(array, start, lengths, strides, out nint linearOffset); _reference = ref (array is not null) @@ -298,7 +298,7 @@ public Span GetSpan(scoped ReadOnlySpan startIndexes, int length) /// public TensorSpan Slice(params scoped ReadOnlySpan startIndexes) { - TensorShape shape = _shape.Slice(startIndexes, out nint linearOffset); + TensorShape shape = _shape.Slice(startIndexes, out nint linearOffset); return new TensorSpan( ref Unsafe.Add(ref _reference, linearOffset), shape @@ -464,46 +464,55 @@ TensorSpan IReadOnlyTensor, T>.ToDenseTensor() public ref struct Enumerator : IEnumerator { private readonly TensorSpan _span; - private readonly nint[] _indexes; private nint _linearOffset; private nint _itemsEnumerated; + private bool _hasCurrent; internal Enumerator(TensorSpan span) { _span = span; - _indexes = new nint[span.Rank]; - - _indexes[^1] = -1; - - _linearOffset = 0 - (!span.IsEmpty ? span.Strides[^1] : 0); + _linearOffset = 0; _itemsEnumerated = 0; + _hasCurrent = false; } /// Gets the element at the current position of the enumerator. - public readonly ref T Current => ref Unsafe.Add(ref _span._reference, _linearOffset); + public readonly ref T Current + { + get + { + if (!_hasCurrent) + { + ThrowHelper.ThrowInvalidOperation_EnumerationNotPositioned(); + } + return ref Unsafe.Add(ref _span._reference, _linearOffset); + } + } /// Advances the enumerator to the next element of the span. public bool MoveNext() { if (_itemsEnumerated == _span._shape.FlattenedLength) { + _hasCurrent = false; return false; } - _linearOffset = _span._shape.AdjustToNextIndex(_span._shape, _linearOffset, _indexes); + _linearOffset = _span.IsDense + ? _itemsEnumerated + : _span._shape.GetLinearOffsetForDimension(_itemsEnumerated, _span.Rank); _itemsEnumerated++; + _hasCurrent = true; return true; } /// Sets the enumerator to its initial position, which is before the first element in the tensor span. public void Reset() { - Array.Clear(_indexes); - _indexes[^1] = -1; - - _linearOffset = 0 - (!_span.IsEmpty ? _span.Strides[^1] : 0); + _linearOffset = 0; _itemsEnumerated = 0; + _hasCurrent = false; } // diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor_1.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor_1.cs index b475cd2a2e66e4..410af2118870b1 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor_1.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor_1.cs @@ -33,6 +33,7 @@ internal Tensor(scoped ReadOnlySpan lengths, scoped ReadOnlySpan str internal Tensor(T[]? array) { + ThrowHelper.ThrowIfArrayTypeMismatch(array); _shape = TensorShape.Create(array); _values = (array is not null) ? array : []; _start = 0; @@ -40,14 +41,16 @@ internal Tensor(T[]? array) internal Tensor(T[]? array, scoped ReadOnlySpan lengths, scoped ReadOnlySpan strides) { + ThrowHelper.ThrowIfArrayTypeMismatch(array); _shape = TensorShape.Create(array, lengths, strides); _values = (array is not null) ? array : []; _start = 0; } - internal Tensor(T[]? array, int start, scoped ReadOnlySpan lengths, scoped ReadOnlySpan strides) + internal Tensor(T[]? array, int start, scoped ReadOnlySpan lengths, scoped ReadOnlySpan strides, bool pinned = false) { - _shape = TensorShape.Create(array, start, lengths, strides); + ThrowHelper.ThrowIfArrayTypeMismatch(array); + _shape = TensorShape.Create(array, start, lengths, strides, pinned); _values = (array is not null) ? array : []; _start = start; } @@ -193,7 +196,10 @@ public ref T GetPinnableReference() { // Ensure that the native code has just one forward branch that is predicted-not-taken. ref T ret = ref Unsafe.NullRef(); - if (_shape.FlattenedLength != 0) ret = ref MemoryMarshal.GetArrayDataReference(_values); + if (_shape.FlattenedLength != 0) + { + ret = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_values), _start); + } return ref ret; } @@ -214,7 +220,7 @@ public unsafe MemoryHandle GetPinnedHandle() /// public Tensor Slice(params ReadOnlySpan startIndexes) { - TensorShape shape = _shape.Slice(startIndexes, out nint linearOffset); + TensorShape shape = _shape.Slice(startIndexes, out nint linearOffset); // The source tensor can have no more than int.MaxValue elements so linearOffset will always be in range of int. Debug.Assert((int)(linearOffset) == linearOffset); @@ -374,46 +380,55 @@ public Tensor ToDenseTensor() public struct Enumerator : IEnumerator { private readonly Tensor _tensor; - private nint[] _indexes; private nint _linearOffset; private nint _itemsEnumerated; + private bool _hasCurrent; internal Enumerator(Tensor tensor) { _tensor = tensor; - _indexes = new nint[tensor.Rank]; - - _indexes[^1] = -1; - - _linearOffset = tensor._start - (!tensor.IsEmpty ? tensor.Strides[^1] : 0); + _linearOffset = tensor._start; _itemsEnumerated = 0; + _hasCurrent = false; } /// - public readonly ref T Current => ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_tensor._values), _linearOffset); + public readonly ref T Current + { + get + { + if (!_hasCurrent) + { + ThrowHelper.ThrowInvalidOperation_EnumerationNotPositioned(); + } + return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_tensor._values), _linearOffset); + } + } /// public bool MoveNext() { if (_itemsEnumerated == _tensor._shape.FlattenedLength) { + _hasCurrent = false; return false; } - _linearOffset = _tensor._shape.AdjustToNextIndex(_tensor._shape, _linearOffset, _indexes); + _linearOffset = _tensor._start + (_tensor.IsDense + ? _itemsEnumerated + : _tensor._shape.GetLinearOffsetForDimension(_itemsEnumerated, _tensor.Rank)); _itemsEnumerated++; + _hasCurrent = true; return true; } /// public void Reset() { - Array.Clear(_indexes); - _indexes[^1] = -1; - - _linearOffset = _tensor._start - (!_tensor.IsEmpty ? _tensor.Strides[^1] : 0); + _linearOffset = _tensor._start; _itemsEnumerated = 0; + _hasCurrent = false; } // diff --git a/src/libraries/System.Numerics.Tensors/src/System/ThrowHelper.cs b/src/libraries/System.Numerics.Tensors/src/System/ThrowHelper.cs index 03e638123f1e29..b15e33b684e9b2 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/ThrowHelper.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/ThrowHelper.cs @@ -27,9 +27,25 @@ public static void ThrowArgument_SpansMustBeNonEmpty() => public static void ThrowArgument_InputAndDestinationSpanMustNotOverlap() => throw new ArgumentException(SR.Argument_InputAndDestinationSpanMustNotOverlap, "destination"); - public static void ThrowIfArrayTypeMismatch(Array? array) + [DoesNotReturn] + public static void ThrowArgument_OverlappingTensorLayoutsNotSupported() => + throw new ArgumentException(SR.Argument_OverlappingTensorLayoutsNotSupported, "destination"); + + public static void ThrowIfArrayTypeMismatch(Array? array, bool isReadOnly) { - if ((array is not null) && (array.GetType().GetElementType() != typeof(T))) + if (array is null) + { + return; + } + + Type arrayType = array.GetType(); + if (arrayType.GetElementType() == typeof(T)) + { + return; + } + + if ((array is not T[] && !typeof(T).MakeArrayType(array.Rank).IsInstanceOfType(array)) || + (!isReadOnly && !typeof(T).IsValueType)) { ThrowArrayTypeMismatchException(); } @@ -71,6 +87,12 @@ internal static void ThrowIndexOutOfRangeException() throw new IndexOutOfRangeException(); } + [DoesNotReturn] + internal static void ThrowInvalidOperation_EnumerationNotPositioned() + { + throw new InvalidOperationException(SR.InvalidOperation_EnumerationNotPositioned); + } + [DoesNotReturn] public static void ThrowArgument_LengthsMustEqualArrayLength() { @@ -140,6 +162,12 @@ public static void ThrowArgument_DimensionsNotSame(string? paramNames) throw new ArgumentException(SR.ThrowArgument_DimensionsNotSame, paramNames); } + [DoesNotReturn] + public static void ThrowArgument_DestinationHasOverlappingElements(string paramName) + { + throw new ArgumentException(SR.ThrowArgument_DestinationHasOverlappingElements, paramName); + } + [DoesNotReturn] public static void ThrowArgument_1DTensorRequired(string? paramNames) { diff --git a/src/libraries/System.Numerics.Tensors/tests/ReadOnlyTensorSpanTests.cs b/src/libraries/System.Numerics.Tensors/tests/ReadOnlyTensorSpanTests.cs index 0490d75771c061..32f8918eba8304 100644 --- a/src/libraries/System.Numerics.Tensors/tests/ReadOnlyTensorSpanTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/ReadOnlyTensorSpanTests.cs @@ -9,14 +9,207 @@ namespace System.Numerics.Tensors.Tests { public class ReadOnlyTensorSpanTests { + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void ReadOnlyTensorSpanOverlappingStridedCopyThrowsBeforeWriting(bool flatten) + { + int[] data = [1, 2, 3, 4, 5, 6]; + + if (flatten) + { + Assert.Throws(() => + new ReadOnlyTensorSpan(data, [2, 2], [3, 1]).FlattenTo(data.AsSpan(2, 4))); + } + else + { + Assert.Throws(() => + new ReadOnlyTensorSpan(data, [2, 2], [3, 1]).CopyTo(new TensorSpan(data, 2, [2, 2], []))); + } + + Assert.Equal([1, 2, 3, 4, 5, 6], data); + } + + [Fact] + public static void TensorAuditCopiedReadOnlyEnumeratorKeepsIndependentPosition() + { + ReadOnlyTensorSpan tensor = new ReadOnlyTensorSpan([10, 20, 99, 30, 40], [2, 2], [3, 1]); + ReadOnlyTensorSpan.Enumerator first = tensor.GetEnumerator(); + Assert.True(first.MoveNext()); + ReadOnlyTensorSpan.Enumerator second = first; + + for (int i = 0; i < 3; i++) + { + second.Reset(); + Assert.True(second.MoveNext()); + Assert.True(second.MoveNext()); + Assert.True(first.MoveNext()); + } + + Assert.Equal(40, first.Current); + } + + [Fact] + public static void ReadOnlyTensorSpanOverlappingBroadcastThrowsBeforeWriting() + { + int[] data = [1, 2, 3, 4]; + + Assert.Throws(() => + new ReadOnlyTensorSpan(data, 1, [1, 2], [0, 1]).CopyTo(new TensorSpan(data, [2, 2]))); + + Assert.Equal([1, 2, 3, 4], data); + } + + [Fact] + public static void ReadOnlyTensorSpanOverlappingReferenceCopyThrowsBeforeWriting() + { + string[] data = ["a", "b", "c", "d", "e", "f"]; + + Assert.Throws(() => + new ReadOnlyTensorSpan(data, [2, 2], [3, 1]).FlattenTo(data.AsSpan(2, 4))); + + Assert.Equal(["a", "b", "c", "d", "e", "f"], data); + } + + [Fact] + public static void ReadOnlyTensorSpanCopyToIdenticalStridedViewNeedsNoSnapshot() + { + int[] data = [1, 2, 3, 4, 5, 6]; + ReadOnlyTensorSpan source = new ReadOnlyTensorSpan(data, [2, 2], [3, 1]); + Assert.True(source.TryCopyTo(new TensorSpan(data, [2, 2], [3, 1]))); + Assert.Equal([1, 2, 3, 4, 5, 6], data); + + int[] repeated = [7]; + Assert.True(new ReadOnlyTensorSpan(repeated, [3], [0]) + .TryCopyTo(new TensorSpan(repeated, [3], [0]))); + Assert.Equal([7], repeated); + } + + [Fact] + public static void ReadOnlyTensorSpanDenseOverlapUsesMemmove() + { + int[] data = [1, 2, 3, 4, 5]; + new ReadOnlyTensorSpan(data, 0, [4], []).CopyTo(new TensorSpan(data, 1, [4], [])); + Assert.Equal([1, 1, 2, 3, 4], data); + } + + [Fact] + public static void EmptyReadOnlyTensorSpanCannotExposeData() + { + ReadOnlyTensorSpan empty = default; + Assert.False(empty.TryGetSpan(ReadOnlySpan.Empty, 1, out _)); + Assert.Equal(0, empty.Slice(ReadOnlySpan.Empty).FlattenedLength); + Assert.True(empty.TryFlattenTo(Span.Empty)); + + ReadOnlyTensorSpan.Enumerator enumerator = empty.GetEnumerator(); + Assert.False(enumerator.MoveNext()); + Assert.Throws(static () => + { + ReadOnlyTensorSpan.Enumerator current = default(ReadOnlyTensorSpan).GetEnumerator(); + _ = current.Current; + }); + Assert.Throws(static () => + { + ReadOnlyTensorSpan.Enumerator current = new ReadOnlyTensorSpan([7]).GetEnumerator(); + _ = current.Current; + }); + } + + [Fact] + public static unsafe void ExplicitShapePointerRejectsNegativeLength() + { + Assert.Throws(static () => + { + int* value = stackalloc int[1]; + _ = new ReadOnlyTensorSpan(value, -1, [1]); + }); + Assert.Throws(static () => + { + int* value = stackalloc int[1]; + _ = new TensorSpan(value, -1, [1]); + }); + } + + [Fact] + public static void NegativePrintLimitIsRejected() + { + Assert.Throws(static () => + new ReadOnlyTensorSpan(new int[] { 123 }, [0]).ToString([-1])); + Assert.Throws(static () => + new ReadOnlyTensorSpan(new int[] { 123 }, [1]).ToString([-1])); + } + + [Theory] + [InlineData(0, new int[] { 1, 2 }, 8, "1, 1, 1, 1, 1, 1, 1, 1")] + [InlineData(4, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, 3, "0, 4, 8")] + public static void ToStringUsesInnermostStride(int stride, int[] data, int length, string expected) + { + ReadOnlyTensorSpan tensor = new ReadOnlyTensorSpan(data, [length], [stride]); + Assert.Contains($"[{expected}]", tensor.ToString([length])); + } + + [Fact] + public static void InvalidShapeAndSliceThrowDocumentedExceptions() + { + int[] data = new int[16]; + Assert.Throws(() => + new ReadOnlyTensorSpan(data, [nint.MaxValue, 2], [])); + Assert.Throws(() => + new ReadOnlyTensorSpan(data.AsSpan(), [nint.MaxValue, 2], [])); + Assert.Throws(() => + new ReadOnlyTensorSpan((Array)data, [0], [nint.MaxValue, 2], [])); + Assert.Throws(static () => + { + unsafe + { + int* pointer = stackalloc int[1]; + _ = new ReadOnlyTensorSpan(pointer, 1, [nint.MaxValue, 2], []); + } + }); + ReadOnlyTensorSpan highRank = new ReadOnlyTensorSpan( + data, [1, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0]); + Assert.Equal(1, highRank.FlattenedLength); + Assert.Throws(() => + new ReadOnlyTensorSpan(data, [nint.MaxValue, 2, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0])); + Assert.Throws(() => + new ReadOnlyTensorSpan(data, [1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0])); + + Assert.Throws(() => + new ReadOnlyTensorSpan(data, [4, 4], []).Slice([5, 0])); + Assert.Throws(() => + new ReadOnlyTensorSpan(data, [4, 4], [])[5, 0]); + } + + [Fact] + public static void ReshapeHandlesZeroDimensions() + { + int[] data = [1, 2, 3, 4]; + Assert.Throws(() => new ReadOnlyTensorSpan(data).Reshape([-1, 0])); + + ReadOnlyTensorSpan empty = Tensor.CreateFromShape([0, 0, 7]).AsReadOnlyTensorSpan(); + ReadOnlyTensorSpan reshaped = empty.Reshape([0, 1, 0, 1]); + Assert.Equal([0, 1, 0, 1], reshaped.Lengths); + Assert.Equal(0, reshaped.FlattenedLength); + Assert.Throws(() => new ReadOnlyTensorSpan(Array.Empty()).Reshape([0, -1])); + + ReadOnlyTensorSpan broadcast = Tensor.Create([1, 2], [2, 2], [0, 1]).AsReadOnlyTensorSpan(); + ReadOnlyTensorSpan withoutSingleton = broadcast.Reshape([1, 2, 2]).Reshape([2, 2]); + Assert.Equal([0, 1], withoutSingleton.Strides); + Assert.Equal(2, withoutSingleton[1, 1]); + } + [Fact] public static void ReadOnlyTensorSpanSystemArrayConstructorTests() { - // When using System.Array constructor make sure the type of the array matches T[] + // When using System.Array constructor make sure incompatible element types are rejected Assert.Throws(() => new TensorSpan(array: new[] { 1 })); + Assert.Throws(() => new ReadOnlyTensorSpan((Array)new byte[8])); + Assert.Throws(() => new ReadOnlyTensorSpan((Array)new byte[2, 4], [0, 0], [2, 4], [])); string[] stringArray = { "a", "b", "c" }; Assert.Throws(() => new TensorSpan(array: stringArray)); + Assert.Equal("a", new ReadOnlyTensorSpan((Array)stringArray)[0]); + Assert.Equal("a", new ReadOnlyTensorSpan((object[])stringArray)[0]); // Make sure basic T[,] constructor works int[,] a = new int[,] { { 91, 92, -93, 94 } }; @@ -223,6 +416,31 @@ public static void ReadOnlyTensorSpanSystemArrayConstructorTests() // Assert.Equal(94, spanInt[1, 1]); } + [Fact] + public static void ReadOnlyTensorSpanArrayConstructorSupportsArrayAssignments() + { + string[] strings = ["first", "second"]; + Assert.Equal("first", new ReadOnlyTensorSpan((Array)strings)[0]); + Assert.Equal("second", new ReadOnlyTensorSpan((Array)strings, [1], [1], [])[0]); + + string[,] strings2D = { { "first", "second" } }; + Assert.Equal("second", new ReadOnlyTensorSpan((Array)strings2D)[0, 1]); + Assert.Equal("second", new ReadOnlyTensorSpan((Array)strings2D, [0, 1], [1, 1], [])[0, 0]); + + int[] signed = [-1, 2]; + uint[] unsigned = (uint[])(object)signed; + Assert.Equal(unsigned[0], new ReadOnlyTensorSpan((Array)signed)[0]); + Assert.Equal(unsigned[1], new ReadOnlyTensorSpan((Array)signed, [1], [1], [])[0]); + + int[,] signed2D = { { -1, 2 } }; + uint[,] unsigned2D = (uint[,])(object)signed2D; + Assert.Equal(unsigned2D[0, 0], new ReadOnlyTensorSpan((Array)signed2D)[0, 0]); + Assert.Equal(unsigned2D[0, 1], new ReadOnlyTensorSpan((Array)signed2D, [0, 1], [1, 1], [])[0, 0]); + + Assert.Throws(() => new ReadOnlyTensorSpan((Array)signed)); + Assert.Throws(() => new ReadOnlyTensorSpan((Array)signed2D, [0, 0], [1, 2], [])); + } + [Fact] public static void ReadOnlyTensorSpanArrayConstructorTests() { @@ -549,6 +767,12 @@ public static void ReadOnlyTensorSpanSpanConstructorTests() [Fact] public static unsafe void ReadOnlyTensorSpanPointerConstructorTests() { + Assert.Throws(static () => + { + int* data = stackalloc int[1]; + _ = new ReadOnlyTensorSpan(data, -1); + }); + // Make sure basic T[] constructor works Span a = [91, 92, -93, 94]; ReadOnlyTensorSpan spanInt; diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorDimensionSpanTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorDimensionSpanTests.cs index 3906407b54b074..cffab631ccf1d4 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorDimensionSpanTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorDimensionSpanTests.cs @@ -1,9 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Xunit; namespace System.Numerics.Tensors.Tests { public class TensorDimensionSpanTests { + [Theory] + [InlineData(0, 2, new int[] { 3, 4 })] + [InlineData(1, 4, new int[] { 4 })] + public void TensorDimensionSpan_ConvertsToReadOnlyView(int dimension, int expectedLength, int[] expectedSlice) + { + Tensor tensor = Tensor.Create([1, 2, 3, 4], [2, 2]); + TensorDimensionSpan mutableView = tensor.GetDimensionSpan(dimension); + ReadOnlyTensorDimensionSpan readonlyView = mutableView; + + Assert.Equal(expectedLength, readonlyView.Length); + ReadOnlyTensorSpan slice = readonlyView[readonlyView.Length - 1]; + int[] actualSlice = new int[slice.FlattenedLength]; + slice.FlattenTo(actualSlice); + Assert.Equal(expectedSlice, actualSlice); + } + + [Fact] + public void TensorAuditDimensionSpanAllowsEmptyRows() + { + Tensor tensor = Tensor.CreateFromShape([2, 0]); + TensorDimensionSpan rows = tensor.GetDimensionSpan(0); + ReadOnlyTensorDimensionSpan readOnlyRows = tensor.AsReadOnlyTensorSpan().GetDimensionSpan(0); + + Assert.Equal(2, rows.Length); + Assert.Equal([0], rows[0].Lengths); + Assert.Equal([0], rows[1].Lengths); + Assert.Equal([0], readOnlyRows[0].Lengths); + Assert.Equal([0], readOnlyRows[1].Lengths); + } + + [Fact] + public void TensorAuditDimensionSpanRejectsOverflowedSliceCount() + { + Assert.Throws(static () => + { + TensorSpan tensor = new TensorSpan(Array.Empty(), [nint.MaxValue, 2, 0]); + _ = tensor.GetDimensionSpan(1); + }); + Assert.Throws(static () => + { + ReadOnlyTensorSpan tensor = new ReadOnlyTensorSpan(Array.Empty(), [nint.MaxValue, 2, 0]); + _ = tensor.GetDimensionSpan(1); + }); + + TensorSpan empty = new TensorSpan(Array.Empty(), [nint.MaxValue, 2, 0]); + Assert.Equal(0, empty.GetDimensionSpan(2).Length); + Assert.Equal(0, ((ReadOnlyTensorSpan)empty).GetDimensionSpan(2).Length); + } + [Fact] public void TensorDimensionSpan_GetDimension_ValidDimension_ReturnsCorrectView() { diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs index f1a1c736e94e16..efbee736d4ea97 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs @@ -12,6 +12,118 @@ namespace System.Numerics.Tensors.Tests { public class TensorSpanTests { + [Fact] + public static void InvalidShapeAndSliceThrowDocumentedExceptions() + { + int[] data = new int[16]; + Assert.Throws(() => + new TensorSpan(data, [nint.MaxValue, 2], [])); + Assert.Throws(() => + new TensorSpan(data.AsSpan(), [nint.MaxValue, 2], [])); + Assert.Throws(() => + new TensorSpan(data, [4, 4], []).Slice([5, 0])); + Assert.Throws(() => + new TensorSpan(data, [4, 4], [])[5, 0]); + } + + [Fact] + public static void ReshapeHandlesZeroDimensions() + { + int[] data = [1, 2, 3, 4]; + Assert.Throws(() => new TensorSpan(data).Reshape([-1, 0])); + + TensorSpan empty = Tensor.CreateFromShape([0, 0, 7]).AsTensorSpan(); + TensorSpan reshaped = empty.Reshape([0, 1, 0, 1]); + Assert.Equal([0, 1, 0, 1], reshaped.Lengths); + Assert.Equal(0, reshaped.FlattenedLength); + Assert.Throws(() => new TensorSpan(Array.Empty()).Reshape([0, -1])); + + TensorSpan broadcast = Tensor.Create([1, 2], [2, 2], [0, 1]).AsTensorSpan(); + TensorSpan withSingleton = broadcast.Reshape([2, 1, 2]); + Assert.Equal([0, 0, 1], withSingleton.Strides); + Assert.Equal(2, withSingleton[1, 0, 1]); + } + + [Fact] + public static void EmptyTensorSpanCannotExposeData() + { + TensorSpan empty = default; + Assert.False(empty.TryGetSpan(ReadOnlySpan.Empty, 1, out Span _)); + Assert.Equal(0, empty.Slice(ReadOnlySpan.Empty).FlattenedLength); + empty.Clear(); + TensorSpan.Enumerator enumerator = empty.GetEnumerator(); + Assert.False(enumerator.MoveNext()); + Assert.Throws(static () => + { + TensorSpan.Enumerator current = default(TensorSpan).GetEnumerator(); + _ = current.Current; + }); + + Assert.Throws(static () => + { + TensorSpan.Enumerator current = new TensorSpan([7]).GetEnumerator(); + _ = current.Current; + }); + } + + [Fact] + public static void SingletonSliceRemainsContiguous() + { + TensorSpan tensor = new TensorSpan([1, 2, 3, 4], [2, 1, 2]); + Assert.Equal([1, 2, 3, 4], tensor.GetSpan([0, 0, 0], 4).ToArray()); + TensorSpan slice = tensor.Slice(new NRange[] { .., .., .. }); + Assert.True(slice.IsDense); + int[] flattened = new int[4]; + slice.Reshape([4]).FlattenTo(flattened); + Assert.Equal([1, 2, 3, 4], flattened); + } + + [Fact] + public static void TryBroadcastToReturnsFalseWithoutWriting() + { + ReadOnlyTensorSpan source = new int[] { 1, 2 }; + int[] data = [7, 7, 7]; + TensorSpan destination = new TensorSpan(data); + Assert.False(source.TryBroadcastTo(destination)); + Assert.Equal([7, 7, 7], data); + } + + [Fact] + public static void TensorAuditBroadcastToEmptyDestination() + { + ReadOnlyTensorSpan source = new ReadOnlyTensorSpan([1, 2, 3], [1, 3]); + TensorSpan destination = new TensorSpan(Array.Empty(), [0, 3]); + + Tensor.BroadcastTo(source, destination); + Assert.True(source.TryBroadcastTo(destination)); + Assert.Equal([0, 3], destination.Lengths); + + source = new ReadOnlyTensorSpan([1, 2], [2, 1]); + destination = new TensorSpan(Array.Empty(), [2, 0]); + Tensor.BroadcastTo(source, destination); + Assert.True(source.TryBroadcastTo(destination)); + Assert.Equal([2, 0], destination.Lengths); + } + + [Fact] + public static void TensorAuditCopiedMutableEnumeratorKeepsIndependentPosition() + { + TensorSpan tensor = new TensorSpan([10, 20, 99, 30, 40], [2, 2], [3, 1]); + TensorSpan.Enumerator first = tensor.GetEnumerator(); + Assert.True(first.MoveNext()); + TensorSpan.Enumerator second = first; + + for (int i = 0; i < 3; i++) + { + second.Reset(); + Assert.True(second.MoveNext()); + Assert.True(second.MoveNext()); + Assert.True(first.MoveNext()); + } + + Assert.Equal(40, first.Current); + } + #region TensorPrimitivesForwardsTests private void FillTensor(Span span) where T : INumberBase @@ -291,7 +403,9 @@ public void TensorExtensionsTwoSpanInSpanOut(TensorPrimitivesTwoSpanInSpanOut NRange[] sliceLengths = Helpers.TensorSliceShapesForBroadcast[index].Select(i => new NRange(0, i)).ToArray(); nint sliceFlattenedLength = CalculateTotalLength(Helpers.TensorSliceShapesForBroadcast[index]); //destination = destination.Slice(sliceLengths); - x.Slice(sliceLengths).BroadcastTo(x); + T[] xSlice = new T[sliceFlattenedLength]; + x.Slice(sliceLengths).FlattenTo(xSlice); + new ReadOnlyTensorSpan(xSlice, Helpers.TensorSliceShapesForBroadcast[index]).BroadcastTo(x); x.FlattenTo(data1); results = tensorOperation(x.Slice(sliceLengths), y, destination); @@ -315,7 +429,9 @@ public void TensorExtensionsTwoSpanInSpanOut(TensorPrimitivesTwoSpanInSpanOut } // Now test if the second source is sliced to be smaller than the first (but is broadcast compatible) that broadcasting happens). - y.Slice(sliceLengths).BroadcastTo(y); + T[] ySlice = new T[sliceFlattenedLength]; + y.Slice(sliceLengths).FlattenTo(ySlice); + new ReadOnlyTensorSpan(ySlice, Helpers.TensorSliceShapesForBroadcast[index]).BroadcastTo(y); y.FlattenTo(data2); results = tensorOperation(x, y.Slice(sliceLengths), destination); @@ -468,7 +584,7 @@ public static unsafe void TensorSpanSetSliceTests() [Fact] public static void TensorSpanSystemArrayConstructorTests() { - // When using System.Array constructor make sure the type of the array matches T[] + // When using System.Array constructor make sure incompatible element types are rejected Assert.Throws(() => new TensorSpan(array: new[] { 1 })); string[] stringArray = { "a", "b", "c" }; @@ -846,6 +962,36 @@ public static void TensorSpanArrayConstructorTests() }); } + [Fact] + public static void TensorSpanArrayConstructorSupportsCompatibleValueTypeArrays() + { + string[] strings = ["first", "second"]; + Assert.Throws(() => new TensorSpan((Array)strings)); + Assert.Throws(() => new TensorSpan((Array)strings, [1], [1], [])); + + string[,] strings2D = { { "first", "second" } }; + Assert.Throws(() => new TensorSpan((Array)strings2D)); + Assert.Throws(() => new TensorSpan((Array)strings2D, [0, 1], [1, 1], [])); + + int[] signed = [-1, 2]; + uint[] unsigned = (uint[])(object)signed; + TensorSpan unsignedSpan = new TensorSpan((Array)signed); + Assert.Equal(unsigned[0], unsignedSpan[0]); + unsignedSpan = new TensorSpan((Array)signed, [1], [1], []); + unsignedSpan[0] = 7; + Assert.Equal(7, signed[1]); + + int[,] signed2D = { { -1, 2 } }; + uint[,] unsigned2D = (uint[,])(object)signed2D; + Assert.Equal(unsigned2D[0, 0], new TensorSpan((Array)signed2D)[0, 0]); + TensorSpan unsigned2DSpan = new TensorSpan((Array)signed2D, [0, 1], [1, 1], []); + unsigned2DSpan[0, 0] = 7; + Assert.Equal(7, signed2D[0, 1]); + + Assert.Throws(() => new TensorSpan((Array)signed)); + Assert.Throws(() => new TensorSpan((Array)signed2D, [0, 0], [1, 2], [])); + } + [Fact] public static void TensorSpanSpanConstructorTests() { diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs index a3492a2caeef0f..417c6470a4c6f9 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs @@ -1304,6 +1304,521 @@ public static void TensorBroadcastTests() Assert.Equal(2, t2[2, 2]); } + [Fact] + public static void TensorBroadcastEmptyDimensionsTests() + { + Tensor source = Tensor.Create([1, 2, 3], [1, 3]); + Tensor empty = Tensor.Broadcast(source, (ReadOnlySpan)[0, 3]); + Assert.Equal([0, 3], empty.Lengths); + Assert.Empty(empty.ToArray()); + + Tensor result = Tensor.Add(empty, source); + Assert.Equal([0, 3], result.Lengths); + Assert.False(source.TryCopyTo(empty.AsTensorSpan())); + + Tensor mismatched = Tensor.CreateFromShape([0, 4]); + Assert.Throws(() => Tensor.Add(empty, mismatched)); + } + + [Fact] + public static void TensorDefaultEmptyDoesNotBroadcastToNonempty() + { + Tensor nonempty = Tensor.Create([1, 2]); + + Assert.Throws(() => Tensor.Add(Tensor.Empty, nonempty)); + Assert.Throws(() => Tensor.Broadcast(Tensor.Empty, (ReadOnlySpan)[2])); + } + + [Fact] + public static void TensorPairwiseReductionsUseBroadcastShape() + { + ReadOnlyTensorSpan column = new ReadOnlyTensorSpan([1, 2, 3], [3, 1], []); + ReadOnlyTensorSpan row = new ReadOnlyTensorSpan([9, 9, 9, 3], [1, 4], []); + + Assert.True(Tensor.EqualsAny(column, row)); + Assert.True(Tensor.EqualsAny(row, column)); + Assert.False(Tensor.LessThanAll(column, row)); + Assert.False(Tensor.GreaterThanAll(row, column)); + + ReadOnlyTensorSpan highRank = new ReadOnlyTensorSpan([1, 2, 3], [1, 1, 1, 1, 3, 1], []); + ReadOnlyTensorSpan vector = new ReadOnlyTensorSpan([9, 9, 9, 3]); + Assert.True(Tensor.EqualsAny(vector, highRank)); + Assert.False(Tensor.LessThanAll(highRank, vector)); + } + + [Fact] + public static void TensorAnyComparisonOfEmptyIsFalse() + { + ReadOnlyTensorSpan empty = new ReadOnlyTensorSpan(new int[4], [2, 0], []); + Assert.False(Tensor.EqualsAny(empty, empty)); + Assert.False(Tensor.EqualsAny(empty, 0)); + Assert.False(Tensor.GreaterThanAny(empty, empty)); + Assert.False(Tensor.GreaterThanAny(empty, 0)); + Assert.True(Tensor.EqualsAll(empty, empty)); + + ReadOnlyTensorSpan emptyColumn = new ReadOnlyTensorSpan(new int[1], [0, 1], []); + ReadOnlyTensorSpan row = new ReadOnlyTensorSpan([1, 2, 3], [1, 3], []); + Assert.False(Tensor.EqualsAny(emptyColumn, row)); + Assert.True(Tensor.EqualsAll(emptyColumn, row)); + } + + [Fact] + public static void TensorNumericReductionsUseBroadcastShape() + { + ReadOnlyTensorSpan column = new ReadOnlyTensorSpan([1, 2], [2, 1], []); + ReadOnlyTensorSpan row = new ReadOnlyTensorSpan([10, 20], [1, 2], []); + Assert.Equal(90, Tensor.Dot(column, row)); + Assert.Equal(90, Tensor.Dot(row, column)); + + ReadOnlyTensorSpan empty = new ReadOnlyTensorSpan(new int[1], [0, 1], []); + Assert.Equal(0, Tensor.Dot(empty, row)); + ReadOnlyTensorSpan emptyDoubles = new ReadOnlyTensorSpan(new double[1], [0, 1], []); + ReadOnlyTensorSpan doubleRow = new ReadOnlyTensorSpan([10.0, 20.0], [1, 2], []); + Assert.Equal(0.0, Tensor.Distance(emptyDoubles, doubleRow)); + Assert.Equal(0, Tensor.Dot(ReadOnlyTensorSpan.Empty, ReadOnlyTensorSpan.Empty)); + } + + [Fact] + public static void AllocatingBinaryOperationsValidateAndBroadcast() + { + ReadOnlyTensorSpan column = new ReadOnlyTensorSpan([1f, 2f], [2, 1], []); + ReadOnlyTensorSpan row = new ReadOnlyTensorSpan([-1f, -1f], [1, 2], []); + Assert.Equal([2, 2], Tensor.Atan2(column, row).Lengths); + Assert.Equal([2, 2], Tensor.Atan2Pi(column, row).Lengths); + Assert.Equal([2, 2], Tensor.CopySign(column, row).Lengths); + + ReadOnlyTensorSpan bitColumn = new ReadOnlyTensorSpan([1, 2], [2, 1], []); + ReadOnlyTensorSpan bitRow = new ReadOnlyTensorSpan([3, 3], [1, 2], []); + Assert.Equal([2, 2], Tensor.BitwiseAnd(bitColumn, bitRow).Lengths); + Assert.Equal([2, 2], Tensor.BitwiseOr(bitColumn, bitRow).Lengths); + Assert.Equal([2, 2], Tensor.Xor(bitColumn, bitRow).Lengths); + + Assert.Throws(() => Tensor.Atan2( + new ReadOnlyTensorSpan([1, 2, 3]), + new ReadOnlyTensorSpan([-1, -1], [2]))); + Assert.Throws(() => Tensor.CopySign( + new ReadOnlyTensorSpan([1, 2, 3]), + new ReadOnlyTensorSpan([-1, -1], [2]))); + Assert.Throws(() => Tensor.BitwiseAnd( + new ReadOnlyTensorSpan([1, 2, 3]), + new ReadOnlyTensorSpan([1, 2], [2]))); + } + + [Theory] + [InlineData(0, -8)] + [InlineData(1, -8)] + [InlineData(2, 3)] + [InlineData(3, 3)] + [InlineData(4, 3)] + [InlineData(5, -8)] + public static void ElementwiseExtremaUseSecondOperand(int operation, int expected) + { + ReadOnlyTensorSpan x = new int[] { 3 }; + ReadOnlyTensorSpan y = new int[] { -8 }; + Tensor result = operation switch + { + 0 => Tensor.MaxMagnitude(x, y), + 1 => Tensor.MaxMagnitudeNumber(x, y), + 2 => Tensor.MaxNumber(x, y), + 3 => Tensor.MinMagnitude(x, y), + 4 => Tensor.MinMagnitudeNumber(x, y), + _ => Tensor.MinNumber(x, y), + }; + Assert.Equal(expected, result[0]); + + int[] values = [99]; + TensorSpan destination = new TensorSpan(values); + switch (operation) + { + case 0: Tensor.MaxMagnitude(x, -8, destination); break; + case 1: Tensor.MaxMagnitudeNumber(x, -8, destination); break; + case 2: Tensor.MaxNumber(x, -8, destination); break; + case 3: Tensor.MinMagnitude(x, -8, destination); break; + case 4: Tensor.MinMagnitudeNumber(x, -8, destination); break; + default: Tensor.MinNumber(x, -8, destination); break; + } + Assert.Equal(expected, values[0]); + } + + [Fact] + public static void TensorRejectsCovariantArrays() + { + object[] data = new string[2]; + Assert.Throws(() => Tensor.Create(data)); + Assert.Throws(() => Tensor.Create(data, [2])); + Assert.Throws(() => Tensor.Create(data, 0, [2], [])); + } + + [Fact] + public static void TensorViewOperationsPreservePinning() + { + Tensor tensor = Tensor.CreateFromShape([2, 2], pinned: true); + Assert.True(tensor.Reshape([4]).IsPinned); + Assert.True(tensor.PermuteDimensions([1, 0]).IsPinned); + Assert.True(Tensor.Transpose(tensor).IsPinned); + Assert.True(tensor.Unsqueeze(0).IsPinned); + Assert.True(tensor.Slice((ReadOnlySpan)[1, 0]).Squeeze().IsPinned); + } + + [Fact] + public static void TensorRejectsDuplicatePermutationAxes() + { + Tensor tensor = Tensor.Create([1, 2], [1, 2]); + Assert.Throws(() => tensor.PermuteDimensions([0, 0])); + Assert.Throws(() => tensor.PermuteDimensions([1, 1])); + Assert.Equal([2, 1], tensor.PermuteDimensions([1, 0]).Lengths); + Assert.Throws(() => Tensor.Create([42]).PermuteDimensions([1])); + } + + [Theory] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(64)] + [InlineData(65)] + [InlineData(321)] + public static void TensorPermuteDimensionsAcrossBufferSizes(int rank) + { + nint[] lengths = new nint[rank]; + Array.Fill(lengths, (nint)1); + lengths[0] = 2; + Tensor tensor = Tensor.Create([1, 2], lengths); + int[] dimensions = Enumerable.Range(0, rank).Reverse().ToArray(); + + Tensor permuted = tensor.PermuteDimensions(dimensions); + Assert.Equal(2, permuted.Lengths[^1]); + Assert.Equal([1, 2], permuted.ToArray()); + + dimensions[0] = dimensions[^1]; + Assert.Throws(() => tensor.PermuteDimensions(dimensions)); + } + + [Theory] + [InlineData(5)] + [InlineData(6)] + public static void TensorBroadcastResultAcrossBufferSizes(int rank) + { + nint[] lengths = new nint[rank]; + Array.Fill(lengths, (nint)1); + lengths[^1] = 2; + Tensor tensor = Tensor.Create([1, 2], lengths); + + Tensor result = Tensor.Add(tensor, Tensor.Create([10, 20])); + + Assert.Equal(lengths, result.Lengths); + Assert.Equal([11, 22], result.ToArray()); + } + + [Fact] + public static void TensorConcatenateEmptyLeadingDimensionsAndValidateDestination() + { + Tensor empty = Tensor.CreateFromShape([0, 2]); + Tensor result = Tensor.ConcatenateOnDimension(1, [empty, empty]); + Assert.Equal([0, 4], result.Lengths); + Assert.Equal(0, result.FlattenedLength); + + Tensor first = Tensor.Create([1]); + Tensor second = Tensor.Create([2, 3]); + int[] values = [7, 7, 7]; + Assert.Throws(() => + Tensor.ConcatenateOnDimension(-1, [first, second], new TensorSpan(values, [2], [2]))); + Assert.Equal([7, 7, 7], values); + } + + [Fact] + public static void TensorConcatenateRejectsOverlappingInputs() + { + int[] data = [1, 2, 3, 4]; + Tensor first = Tensor.Create(data, 2, [2], []); + Tensor second = Tensor.Create(data, 0, [2], []); + + Assert.Throws(() => Tensor.ConcatenateOnDimension(-1, [first, second], new TensorSpan(data))); + + Assert.Equal([1, 2, 3, 4], data); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + public static void TensorConcatenateRejectsAliasedDestination(int dimension) + { + int[] backing = [99]; + Tensor first = Tensor.Create([1]); + Tensor second = Tensor.Create([2]); + + Assert.Throws(() => + Tensor.ConcatenateOnDimension(dimension, [first, second], new TensorSpan(backing, [2], [0]))); + Assert.Equal([99], backing); + + int[] gappedBacking = [99, 99, 99]; + TensorSpan gappedDestination = new TensorSpan(gappedBacking, [2], [2]); + Tensor.ConcatenateOnDimension(dimension, [first, second], gappedDestination); + Assert.Equal([1, 99, 2], gappedBacking); + } + + [Fact] + public static void TensorResizeToRejectsAliasedDestination() + { + int[] backing = [99]; + + Assert.Throws(() => + Tensor.ResizeTo(new ReadOnlyTensorSpan([42]), new TensorSpan(backing, [2], [0]))); + Assert.Equal([99], backing); + + int[] gappedBacking = [99, 99, 99]; + Tensor.ResizeTo(new ReadOnlyTensorSpan([42]), new TensorSpan(gappedBacking, [2], [2])); + Assert.Equal([42, 99, 0], gappedBacking); + } + + [Fact] + public static void TensorAuditCopiedEnumeratorKeepsIndependentPosition() + { + Tensor tensor = Tensor.Create([10, 20, 99, 30, 40], [2, 2], [3, 1]); + Tensor.Enumerator first = tensor.GetEnumerator(); + Assert.True(first.MoveNext()); + Tensor.Enumerator second = first; + + for (int i = 0; i < 3; i++) + { + second.Reset(); + Assert.True(second.MoveNext()); + Assert.True(second.MoveNext()); + Assert.True(first.MoveNext()); + } + + Assert.Equal(40, first.Current); + } + + [Fact] + public static void TensorConversionRejectsOverlappingDifferentElementTypes() + { + byte[] data = [1, 2, 0, 0, 0, 0, 0, 0]; + + Assert.Throws(() => + Tensor.ConvertChecked(new ReadOnlyTensorSpan(data.AsSpan(0, 2)), + new TensorSpan(MemoryMarshal.Cast(data.AsSpan())))); + Assert.Equal([1, 2, 0, 0, 0, 0, 0, 0], data); + } + + [Fact] + public static void TensorAuditReverseBroadcastsAndHandlesOverlap() + { + int[] repeated = [7]; + TensorSpan view = new TensorSpan(repeated, [3], [0]); + Tensor.Reverse(view, view); + Assert.Equal([7], repeated); + + int[] destination = [99, 99, 99]; + Tensor.ReverseDimension(new ReadOnlyTensorSpan([7]), new TensorSpan(destination), 0); + Assert.Equal([7, 7, 7], destination); + } + + [Theory] + [InlineData(new int[] { 7 }, new int[] { 2, 3 }, new int[] { 7, 7, 7, 7, 7, 7 })] + [InlineData(new int[] { 1, 2 }, new int[] { 3, 2 }, new int[] { 2, 1, 2, 1, 2, 1 })] + public static void TensorAuditReverseWithRankExpansion(int[] sourceValues, int[] destinationLengths, int[] expected) + { + int[] output = new int[expected.Length]; + Tensor.ReverseDimension( + new ReadOnlyTensorSpan(sourceValues), + new TensorSpan(output, [destinationLengths[0], destinationLengths[1]]), + 0); + Assert.Equal(expected, output); + } + + [Fact] + public static void TensorAuditResizeToEmptyShape() + { + Tensor resized = Tensor.Resize(Tensor.Create([42]), ReadOnlySpan.Empty); + + Assert.Equal([0], resized.Lengths); + Assert.Equal(0, resized.FlattenedLength); + } + + [Fact] + public static void TensorAuditSoftMaxShiftIsRepresentable() + { + double low = 1e16; + double high = low + 708; + Tensor result = Tensor.SoftMax(new ReadOnlyTensorSpan([low, high, high, high, high, high, high])); + + Assert.Equal(0, result[0], 12); + for (int i = 1; i < 7; i++) + { + Assert.Equal(1.0 / 6, result[i], 12); + } + } + + [Theory] + [InlineData(false, 1.0, 0.0, Math.PI / 2)] + [InlineData(true, 1.0, 0.0, 0.5)] + [InlineData(false, -1.0, 0.0, -Math.PI / 2)] + [InlineData(true, -1.0, 0.0, -0.5)] + public static void TensorScalarLeftAtan2PreservesOperandOrder(bool divideByPi, double x, double y, double expected) + { + ReadOnlyTensorSpan source = new ReadOnlyTensorSpan([y]); + Tensor result = divideByPi ? Tensor.Atan2Pi(x, source) : Tensor.Atan2(x, source); + Assert.Equal(expected, result[0], 12); + + double[] destination = [double.NaN]; + if (divideByPi) + { + Tensor.Atan2Pi(x, source, new TensorSpan(destination)); + } + else + { + Tensor.Atan2(x, source, new TensorSpan(destination)); + } + Assert.Equal(expected, destination[0], 12); + } + + [Fact] + public static void TensorOperationsRejectUnsafeOverlap() + { + int[] broadcastBacking = [1]; + Assert.Throws(() => + Tensor.Add(new TensorSpan(broadcastBacking, [3], [0]), 1, + new TensorSpan(broadcastBacking, [3], [0]))); + Assert.Equal(1, broadcastBacking[0]); + + int[] gappedBacking = [1, 99, 99, 99, 2]; + Assert.Throws(() => + Tensor.Add(new TensorSpan(gappedBacking, [2, 2], [0, 4]), 1, + new TensorSpan(gappedBacking, [2, 2], [0, 4]))); + Assert.Equal([1, 99, 99, 99, 2], gappedBacking); + + int[] shifted = [1, 2, 3, 4]; + Assert.Throws(() => + Tensor.Add(new ReadOnlyTensorSpan(shifted, 0, [3], []), 10, + new TensorSpan(shifted, 1, [3], []))); + Assert.Equal([1, 2, 3, 4], shifted); + + shifted = [1, 2, 3, 4]; + Assert.Throws(() => + Tensor.Add(new ReadOnlyTensorSpan(shifted, 0, [3], []), + new ReadOnlyTensorSpan([10, 10, 10]), new TensorSpan(shifted, 1, [3], []))); + Assert.Equal([1, 2, 3, 4], shifted); + + int[] reversed = [1, 2, 3]; + Tensor.Reverse(new ReadOnlyTensorSpan(reversed), new TensorSpan(reversed)); + Assert.Equal([3, 2, 1], reversed); + } + + [Theory] + [InlineData(-1, new int[] { 6, 5, 4, 3, 2, 1 })] + [InlineData(0, new int[] { 4, 5, 6, 1, 2, 3 })] + [InlineData(1, new int[] { 3, 2, 1, 6, 5, 4 })] + public static void TensorReverseDenseInPlaceUsesNoSnapshot(int dimension, int[] expected) + { + int[] data = [1, 2, 3, 4, 5, 6]; + Tensor.ReverseDimension(new ReadOnlyTensorSpan(data, [2, 3]), + new TensorSpan(data, [2, 3]), dimension); + Assert.Equal(expected, data); + } + + [Fact] + public static void TensorReverseBroadcastWithDistinctValuesRejectsInPlace() + { + int[] data = [1, 2]; + Assert.Throws(() => + Tensor.ReverseDimension(new ReadOnlyTensorSpan(data, [2, 2], [0, 1]), + new TensorSpan(data, [2, 2], [0, 1]), 1)); + Assert.Equal([1, 2], data); + } + + [Fact] + public static void TensorReverseMiddleAxisDenseInPlace() + { + int[] data = [1, 2, 3, 4, 5, 6, 7, 8]; + Tensor.ReverseDimension(new ReadOnlyTensorSpan(data, [2, 2, 2]), + new TensorSpan(data, [2, 2, 2]), 1); + Assert.Equal([3, 4, 1, 2, 7, 8, 5, 6], data); + } + + [Theory] + [InlineData(1000.0, 1000.0, 0.5, 0.5)] + [InlineData(-1000.0, -1000.0, 0.5, 0.5)] + [InlineData(-1000.0, 1000.0, 0.0, 1.0)] + [InlineData(1000.0, -1000.0, 1.0, 0.0)] + public static void TensorSoftMaxRemainsFiniteForFiniteInputs(double first, double second, double firstExpected, double secondExpected) + { + double[] data = [first, second]; + Tensor output = Tensor.SoftMax(new ReadOnlyTensorSpan(data)); + Assert.Equal(firstExpected, output[0], 12); + Assert.Equal(secondExpected, output[1], 12); + + Tensor.SoftMax(new ReadOnlyTensorSpan(data), new TensorSpan(data)); + Assert.Equal(firstExpected, data[0], 12); + Assert.Equal(secondExpected, data[1], 12); + } + + [Theory] + [InlineData(new double[] { double.NegativeInfinity, double.NegativeInfinity, 0 }, new double[] { 0, 0, 1 })] + [InlineData(new double[] { 0, double.NegativeInfinity, double.NegativeInfinity }, new double[] { 1, 0, 0 })] + [InlineData(new double[] { double.NegativeInfinity, double.NegativeInfinity, 0, 0 }, new double[] { 0, 0, 0.5, 0.5 })] + public static void TensorSoftMaxHandlesNegativeInfinityWithFiniteValues(double[] input, double[] expected) + { + Tensor result = Tensor.SoftMax(new ReadOnlyTensorSpan(input)); + Assert.Equal(expected, result.ToArray()); + + double[] inPlace = (double[])input.Clone(); + Tensor.SoftMax(new ReadOnlyTensorSpan(inPlace), new TensorSpan(inPlace)); + Assert.Equal(expected, inPlace); + } + + [Fact] + public static void TensorBroadcastToRejectsOverlappingViews() + { + int[] data = [1, 2, 3, 4]; + Assert.Throws(() => + Tensor.BroadcastTo(new ReadOnlyTensorSpan(data, 1, [1, 2], []), new TensorSpan(data, [2, 2]))); + Assert.Equal([1, 2, 3, 4], data); + } + + [Fact] + public static void TensorEmptyEnumerationAndCurrentPosition() + { + Tensor.Enumerator empty = Tensor.Empty.GetEnumerator(); + Assert.False(empty.MoveNext()); + Assert.Throws(() => _ = empty.Current); + Assert.Equal(0, Tensor.Sum(ReadOnlyTensorSpan.Empty)); + + Tensor.Enumerator enumerator = Tensor.Create([7]).GetEnumerator(); + Assert.Throws(() => _ = enumerator.Current); + Assert.True(enumerator.MoveNext()); + Assert.Equal(7, enumerator.Current); + Assert.False(enumerator.MoveNext()); + Assert.Throws(() => _ = enumerator.Current); + } + + [Fact] + public static void TensorSqueezeAllSingletonDimensionsRetainsElement() + { + Tensor tensor = Tensor.Create([42], [1, 1]); + Tensor squeezed = tensor.Squeeze(); + Assert.Equal(1, squeezed.FlattenedLength); + Assert.Equal([42], squeezed.ToArray()); + + TensorSpan mutable = tensor.AsTensorSpan().Squeeze(); + Assert.Equal(1, mutable.FlattenedLength); + Assert.Equal(42, mutable[0]); + + ReadOnlyTensorSpan readonlyView = tensor.AsReadOnlyTensorSpan().SqueezeDimension(0); + readonlyView = readonlyView.SqueezeDimension(0); + Assert.Equal(1, readonlyView.FlattenedLength); + Assert.Equal(42, readonlyView[0]); + } + + [Fact] + public static void TensorInvalidShapeAndSliceThrowDocumentedExceptions() + { + Assert.Throws(() => Tensor.CreateFromShape([nint.MaxValue, 2])); + Tensor tensor = Tensor.Create(new int[16], [4, 4]); + Assert.Throws(() => tensor.Slice((ReadOnlySpan)[5, 0])); + } + [Fact] public static void TensorResizeTests() { @@ -1352,6 +1867,48 @@ public static void TensorResizeTests() Assert.Equal(0, t1[1, 4]); } + [Fact] + public static void TensorResizePreservesPinning() + { + Tensor pinned = Tensor.CreateFromShape([2], pinned: true); + pinned[0] = 7; + + Tensor resized = Tensor.Resize(pinned, [3]); + + Assert.True(resized.IsPinned); + Assert.Equal([7, 0, 0], resized.ToArray()); + } + + [Fact] + public static void TensorResizeToClearsUnwrittenElements() + { + ReadOnlyTensorSpan source = new ReadOnlyTensorSpan(new int[] { 1, 2 }); + int[] dense = [9, 9, 9, 9]; + Tensor.ResizeTo(source, new TensorSpan(dense)); + Assert.Equal([1, 2, 0, 0], dense); + + int[] strided = [9, 9, 9, 9, 9, 9]; + TensorSpan destination = new TensorSpan(strided, [2, 2], [3, 1]); + Tensor.ResizeTo(source, destination); + Assert.Equal([1, 2, 9, 0, 0, 9], strided); + } + + [Fact] + public static void TensorResizeToRejectsOverlappingStridedViews() + { + int[] data = [1, 2, 3, 4, 5, 6]; + + Assert.Throws(() => + Tensor.ResizeTo(new ReadOnlyTensorSpan(data, [2, 2], [3, 1]), new TensorSpan(data, 2, [2, 2], []))); + Assert.Equal([1, 2, 3, 4, 5, 6], data); + + data = [1, 2, 3, 4, 5, 6]; + + Assert.Throws(() => + Tensor.ResizeTo(new ReadOnlyTensorSpan(data.AsSpan(0, 2)), new TensorSpan(data, 1, [2, 2], [3, 1]))); + Assert.Equal([1, 2, 3, 4, 5, 6], data); + } + [Fact] public static void TensorResizeWithStartTests() { @@ -1432,6 +1989,25 @@ public static void TensorSplitTests() Assert.Equal(7, t1[1][1, 1, 0]); } + [Theory] + [InlineData(2)] + [InlineData(6)] + public static void TensorSplitPreservesOutputShapesAfterBufferReuse(int rank) + { + nint[] lengths = new nint[rank]; + Array.Fill(lengths, (nint)1); + lengths[0] = 2; + Tensor source = Tensor.Create([3, 5], lengths); + + Tensor[] result = Tensor.Split(source, 2, 0); + Tensor.Split(source, 2, 0); + + Assert.Equal(1, result[0].Lengths[0]); + Assert.Equal(1, result[1].Lengths[0]); + Assert.Equal(3, result[0].ToArray()[0]); + Assert.Equal(5, result[1].ToArray()[0]); + } + [Fact] public static void TensorReverseTests() { @@ -2042,6 +2618,35 @@ public static void TensorConcatenateTests() } } + [Theory] + [InlineData(2)] + [InlineData(6)] + public static void TensorConcatenateValidatesDestinationWithoutShapeBuffer(int rank) + { + nint[] lengths = new nint[rank]; + Array.Fill(lengths, (nint)1); + lengths[0] = 2; + Tensor first = Tensor.Create([1, 2], lengths); + Tensor second = Tensor.Create([3, 4], lengths); + nint[] resultLengths = [.. lengths]; + resultLengths[^1] = 2; + + Tensor result = Tensor.ConcatenateOnDimension(rank - 1, [first, second]); + Assert.Equal(resultLengths, result.Lengths); + Assert.Equal([1, 3, 2, 4], result.ToArray()); + + TensorSpan destination = new TensorSpan(new int[4], resultLengths, []); + Tensor.ConcatenateOnDimension(rank - 1, [first, second], destination); + int[] actual = new int[4]; + destination.FlattenTo(actual); + Assert.Equal([1, 3, 2, 4], actual); + + resultLengths[0] = 4; + Assert.Throws(() => + Tensor.ConcatenateOnDimension(rank - 1, [first, second], + new TensorSpan(new int[8], resultLengths, []))); + } + [Fact] public static void TensorConcatenateNonDenseDestinationTests() { @@ -2062,6 +2667,40 @@ public static void TensorConcatenateNonDenseDestinationTests() Assert.Equal(4, backing[1, 2]); } + [Theory] + [InlineData(2)] + [InlineData(6)] + public static void TensorConcatenateNonDenseDestinationAlongAxis(int rank) + { + nint[] lengths = new nint[rank]; + Array.Fill(lengths, (nint)1); + Tensor first = Tensor.Create([1], lengths); + Tensor second = Tensor.Create([2], lengths); + + lengths[^1] = 2; + nint[] strides = new nint[rank]; + strides[^1] = 2; + int[] backing = [0, 0, 0]; + TensorSpan destination = new TensorSpan(backing, lengths, strides); + + Tensor.ConcatenateOnDimension(rank - 1, [first, second], destination); + + Assert.Equal([1, 0, 2], backing); + } + + [Fact] + public static void TensorConcatenateNonDenseReferenceDestinationTests() + { + Tensor backing = Tensor.Create(["_", "_", "_", "_", "_", "_"], [2, 3]); + TensorSpan destination = backing.AsTensorSpan().Slice([0..2, 1..3]); + Tensor first = Tensor.Create(["a", "b"]); + Tensor second = Tensor.Create(["c", "d"]); + + Tensor.ConcatenateOnDimension(-1, [first, second], destination); + + Assert.Equal(["_", "a", "b", "_", "c", "d"], backing.ToArray()); + } + [Fact] public static void TensorTransposeTests() { @@ -2878,6 +3517,71 @@ public static void TensorReshapeTest() Assert.Equal(0, tensor.Strides[3]); } + [Fact] + public static void TensorReshapeHandlesZeroDimensions() + { + Tensor tensor = Tensor.Create([1, 2, 3, 4], [4]); + Assert.Throws(() => tensor.Reshape([-1, 0])); + Assert.Throws(() => tensor.Reshape([2, -2])); + Assert.Throws(() => tensor.Reshape([])); + + Tensor empty = Tensor.CreateFromShape([0, 0, 7]); + Tensor reshaped = empty.Reshape([0, 1, 0, 1]); + Assert.Equal([0, 1, 0, 1], reshaped.Lengths); + Assert.Equal(0, reshaped.FlattenedLength); + Assert.Throws(() => empty.Reshape([0, -1])); + Assert.Equal([0], empty.Reshape([-1]).Lengths); + + Tensor broadcast = Tensor.Create([1, 2], [2, 2], [0, 1]); + Assert.Equal([1, 2, 1, 2], broadcast.Reshape([2, 1, 2]).ToArray()); + Assert.Equal([1, 2, 1, 2], broadcast.Reshape([1, 2, 2]).Reshape([2, 2]).ToArray()); + Assert.Equal([1, 2, 3, 4], tensor.Reshape([1, 4]).Reshape([4]).ToArray()); + } + + [Theory] + [InlineData(4, false)] + [InlineData(4, true)] + [InlineData(6, false)] + [InlineData(6, true)] + public static void TensorReshapeUsesIndependentShapeStorage(int rank, bool isBroadcast) + { + nint[] sourceLengths = new nint[rank]; + Array.Fill(sourceLengths, (nint)1); + sourceLengths[0] = 2; + int[] values = isBroadcast ? [42] : [1, 2]; + nint[] sourceStrides = isBroadcast ? new nint[rank] : []; + Tensor source = Tensor.Create(values, sourceLengths, sourceStrides); + + nint[] requestedLengths = new nint[rank]; + Array.Fill(requestedLengths, (nint)1); + requestedLengths[^1] = -1; + nint[] expectedLengths = [.. requestedLengths]; + expectedLengths[^1] = 2; + int[] expectedValues = isBroadcast ? [42, 42] : [1, 2]; + + Tensor reshaped = source.Reshape(requestedLengths); + TensorSpan span = source.AsTensorSpan().Reshape(requestedLengths); + ReadOnlyTensorSpan readOnlySpan = source.AsReadOnlyTensorSpan().Reshape(requestedLengths); + + nint[] alternateLengths = [.. expectedLengths]; + alternateLengths[1] = 2; + alternateLengths[^1] = 1; + source.Reshape(alternateLengths); + + Assert.Equal(-1, requestedLengths[^1]); + Assert.Equal(expectedLengths, reshaped.Lengths); + Assert.Equal(expectedLengths, span.Lengths); + Assert.Equal(expectedLengths, readOnlySpan.Lengths); + Assert.Equal(reshaped.Strides, span.Strides); + Assert.Equal(reshaped.Strides, readOnlySpan.Strides); + Assert.Equal(expectedValues, reshaped.ToArray()); + int[] flattened = new int[2]; + span.FlattenTo(flattened); + Assert.Equal(expectedValues, flattened); + readOnlySpan.FlattenTo(flattened); + Assert.Equal(expectedValues, flattened); + } + [Fact] public static void TensorSqueezeTest() { @@ -3408,6 +4112,20 @@ public void TensorEqualsAllTest() Assert.True(result); } + [Theory] + [InlineData(2)] + [InlineData(6)] + public static void TensorComparisonRentedBufferEarlyExit(int rank) + { + nint[] lengths = new nint[rank]; + Array.Fill(lengths, (nint)1); + lengths[^1] = 2; + Tensor tensor = Tensor.Create([1, 2], lengths); + + Assert.False(Tensor.EqualsAll(tensor.AsReadOnlyTensorSpan(), 1)); + Assert.True(Tensor.GreaterThanAll(tensor.AsReadOnlyTensorSpan(), 0)); + } + [Fact] public void TensorFilteredUpdateTest() { @@ -3494,6 +4212,41 @@ public void TensorGetPinnedHandleTests() } } + [Theory] + [InlineData(false)] + [InlineData(true)] + public unsafe void TensorSlicePinningStartsAtSlice(bool useRanges) + { + Tensor parent = Tensor.Create([91, 92, 11, 12, 21, 22], [3, 2]); + Tensor slice = useRanges + ? parent.Slice(1..3, ..) + : parent.Slice((ReadOnlySpan)[1, 0]); + + Assert.Equal(11, slice.GetPinnableReference()); + Assert.Equal(11, slice.AsTensorSpan().GetPinnableReference()); + + using MemoryHandle handle = slice.GetPinnedHandle(); + Assert.Equal([11, 12, 21, 22], new ReadOnlySpan(handle.Pointer, 4).ToArray()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TensorFlattenToLeavesExtraDestinationUntouched(bool strided) + { + Tensor source = strided + ? Tensor.Create([1, 2, 3, 4, 5], [2, 2], [3, 1]) + : Tensor.Create([1, 2, 4, 5], [2, 2]); + int[] destination = [9, 9, 9, 9, 9, 9]; + + Assert.True(source.TryFlattenTo(destination)); + Assert.Equal([1, 2, 4, 5, 9, 9], destination); + + Array.Fill(destination, 9); + source.AsReadOnlyTensorSpan().FlattenTo(destination); + Assert.Equal([1, 2, 4, 5, 9, 9], destination); + } + [Fact] public void IsDenseTests() { From 55ff14f7adbe4582c614a2d1c82ed3b4266f15bd Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Tue, 29 Sep 2026 21:02:57 -0700 Subject: [PATCH 2/6] Accelerate dense and contiguous Tensor operations Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../System/Numerics/Tensors/netcore/Tensor.cs | 109 ++++++-- .../Tensors/netcore/TensorOperation.cs | 222 ++++++++++++++++ .../tests/TensorSpanTests.cs | 36 +-- .../tests/TensorTests.cs | 245 ++++++++++++++++-- 4 files changed, 565 insertions(+), 47 deletions(-) diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs index 735a55734f6f74..fed118595b789d 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs @@ -427,13 +427,33 @@ public static ref readonly TensorSpan FillGaussianNormalDistribution(in Te } else { - TensorSpan.Enumerator enumerator = destination.GetEnumerator(); - - while (enumerator.MoveNext()) + int dimension = destination.FlattenedLength >= TensorOperation.MinSlicedOperationLength + ? TensorOperation.GetDenseSliceDimension(destination._shape, destination._shape) + : -1; + if (dimension >= 0) { - double u1 = 1.0 - random.NextDouble(); - double u2 = 1.0 - random.NextDouble(); - enumerator.Current = T.CreateChecked(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); + TensorDimensionSpan slices = destination.GetDimensionSpan(dimension); + for (nint i = 0; i < slices.Length; i++) + { + TensorSpan slice = slices[i]; + Span span = MemoryMarshal.CreateSpan(ref slice._reference, (int)slice.FlattenedLength); + for (int j = 0; j < span.Length; j++) + { + double u1 = 1.0 - random.NextDouble(); + double u2 = 1.0 - random.NextDouble(); + span[j] = T.CreateChecked(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); + } + } + } + else + { + TensorSpan.Enumerator enumerator = destination.GetEnumerator(); + while (enumerator.MoveNext()) + { + double u1 = 1.0 - random.NextDouble(); + double u2 = 1.0 - random.NextDouble(); + enumerator.Current = T.CreateChecked(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); + } } } @@ -462,11 +482,29 @@ public static ref readonly TensorSpan FillUniformDistribution(in TensorSpa } else { - TensorSpan.Enumerator enumerator = destination.GetEnumerator(); - - while (enumerator.MoveNext()) + int dimension = destination.FlattenedLength >= TensorOperation.MinSlicedOperationLength + ? TensorOperation.GetDenseSliceDimension(destination._shape, destination._shape) + : -1; + if (dimension >= 0) + { + TensorDimensionSpan slices = destination.GetDimensionSpan(dimension); + for (nint i = 0; i < slices.Length; i++) + { + TensorSpan slice = slices[i]; + Span span = MemoryMarshal.CreateSpan(ref slice._reference, (int)slice.FlattenedLength); + for (int j = 0; j < span.Length; j++) + { + span[j] = T.CreateChecked(random.NextDouble()); + } + } + } + else { - enumerator.Current = T.CreateChecked(random.NextDouble()); + TensorSpan.Enumerator enumerator = destination.GetEnumerator(); + while (enumerator.MoveNext()) + { + enumerator.Current = T.CreateChecked(random.NextDouble()); + } } } @@ -1632,14 +1670,32 @@ public static Tensor Resize(Tensor tensor, ReadOnlySpan lengths) else { nint copyLength = Math.Min(tensor.FlattenedLength, newSize); - ReadOnlyTensorSpan.Enumerator enumerator = tensor.AsReadOnlyTensorSpan().GetEnumerator(); Span ospan = MemoryMarshal.CreateSpan(ref output.AsTensorSpan()._reference, (int)output.FlattenedLength); - - for (nint i = 0; i < copyLength; i++) + ReadOnlyTensorSpan source = tensor.AsReadOnlyTensorSpan(); + int dimension = copyLength >= TensorOperation.MinSlicedOperationLength + ? TensorOperation.GetDenseSliceDimension(source._shape, source._shape) + : -1; + if (dimension >= 0) + { + ReadOnlyTensorDimensionSpan slices = source.GetDimensionSpan(dimension); + int copied = 0; + for (nint i = 0; copied < copyLength; i++) + { + ReadOnlyTensorSpan slice = slices[i]; + int count = (int)Math.Min(slice.FlattenedLength, copyLength - copied); + MemoryMarshal.CreateReadOnlySpan(in slice._reference, count).CopyTo(ospan.Slice(copied, count)); + copied += count; + } + } + else { - bool moved = enumerator.MoveNext(); - Debug.Assert(moved); - ospan[(int)i] = enumerator.Current; + ReadOnlyTensorSpan.Enumerator enumerator = source.GetEnumerator(); + for (nint i = 0; i < copyLength; i++) + { + bool moved = enumerator.MoveNext(); + Debug.Assert(moved); + ospan[(int)i] = enumerator.Current; + } } } @@ -1898,6 +1954,27 @@ public static bool SequenceEqual(this scoped in ReadOnlyTensorSpan tensor, return MemoryMarshal.CreateReadOnlySpan(in tensor.GetPinnableReference(), (int)tensor.FlattenedLength).SequenceEqual(MemoryMarshal.CreateReadOnlySpan(in other.GetPinnableReference(), (int)other.FlattenedLength)); } + int dimension = tensor.FlattenedLength >= TensorOperation.MinSlicedOperationLength + ? TensorOperation.GetDenseSliceDimension(tensor._shape, other._shape) + : -1; + if (dimension >= 0) + { + ReadOnlyTensorDimensionSpan tensorSlices = tensor.GetDimensionSpan(dimension); + ReadOnlyTensorDimensionSpan otherSlices = other.GetDimensionSpan(dimension); + + for (nint i = 0; i < tensorSlices.Length; i++) + { + ReadOnlyTensorSpan tensorSlice = tensorSlices[i]; + ReadOnlyTensorSpan otherSlice = otherSlices[i]; + if (!MemoryMarshal.CreateReadOnlySpan(in tensorSlice._reference, (int)tensorSlice.FlattenedLength) + .SequenceEqual(MemoryMarshal.CreateReadOnlySpan(in otherSlice._reference, (int)otherSlice.FlattenedLength))) + { + return false; + } + } + return true; + } + ReadOnlyTensorSpan.Enumerator enumerator1 = tensor.GetEnumerator(); ReadOnlyTensorSpan.Enumerator enumerator2 = other.GetEnumerator(); diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs index 66128355f65c27..187141d8156902 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs @@ -11,6 +11,9 @@ namespace System.Numerics.Tensors { internal static class TensorOperation { + // Constructing slices costs more than indexed iteration for tiny tensors. + internal const int MinSlicedOperationLength = 32; + private static bool ValidateSourceOverlap( in ReadOnlyTensorSpan source, in TensorSpan destination, bool isCopy = false) { @@ -45,9 +48,96 @@ private static bool ValidateSourceOverlap( throw new ArgumentException(SR.Argument_OverlappingTensorLayoutsNotSupported, nameof(destination)); } + private static ReadOnlySpan AsDenseSpan(in ReadOnlyTensorSpan source) + => MemoryMarshal.CreateReadOnlySpan(in source._reference, checked((int)source.FlattenedLength)); + + private static Span AsDenseSpan(in TensorSpan destination) + => MemoryMarshal.CreateSpan(ref destination._reference, checked((int)destination.FlattenedLength)); + + private static ReadOnlyTensorSpan BroadcastSource(in ReadOnlyTensorSpan source, in TensorShape destination) + { + if (source.Rank > destination.Rank || destination.FlattenedLength == 0) + { + return source; + } + + ReadOnlySpan sourceLengths = source.Lengths; + ReadOnlySpan sourceStrides = source.Strides; + scoped Span strides = RentedBuffer.CreateUninitialized(destination.Rank, out RentedBuffer rentedBuffer); + int rankDifference = destination.Rank - source.Rank; + + for (int i = 0; i < destination.Rank; i++) + { + int sourceIndex = i - rankDifference; + strides[i] = sourceIndex < 0 || sourceLengths[sourceIndex] == 1 ? 0 : sourceStrides[sourceIndex]; + } + + TensorShape shape = TensorShape.Create(in source._reference, source._shape.LinearLength, destination.Lengths, strides, source.IsPinned); + rentedBuffer.Dispose(); + return new ReadOnlyTensorSpan(in source._reference, in shape); + } + + internal static int GetDenseSliceDimension(in TensorShape source, in TensorShape destination, in TensorShape other = default) + { + ReadOnlySpan lengths = destination.Lengths; + if (!source.Lengths.SequenceEqual(lengths) || + (other.Rank != 0 && !other.Lengths.SequenceEqual(lengths)) || + destination.FlattenedLength == 0) + { + return -1; + } + + ReadOnlySpan sourceStrides = source.Strides; + ReadOnlySpan destinationStrides = destination.Strides; + ReadOnlySpan otherStrides = other.Strides; + nint contiguousLength = 1; + int dimension = -1; + + for (int i = lengths.Length - 1; i > 0; i--) + { + nint length = lengths[i]; + if (length > 1 && + (sourceStrides[i] != contiguousLength || + destinationStrides[i] != contiguousLength || + (other.Rank != 0 && otherStrides[i] != contiguousLength))) + { + break; + } + if (length > int.MaxValue / contiguousLength) + { + break; + } + + contiguousLength *= length; + if (contiguousLength > 1) + { + dimension = i - 1; + } + } + + return dimension; + } + public static void Invoke(in TensorSpan x) where TOperation : IOperation { + if (x.IsDense && x.FlattenedLength <= int.MaxValue) + { + TOperation.Invoke(AsDenseSpan(x)); + return; + } + + if (GetDenseSliceDimension(x._shape, x._shape) is int dimension && dimension >= 0) + { + TensorDimensionSpan slices = x.GetDimensionSpan(dimension); + for (nint i = 0; i < slices.Length; i++) + { + TensorSpan slice = slices[i]; + TOperation.Invoke(AsDenseSpan(slice)); + } + return; + } + scoped Span indexes = RentedBuffer.Create(x.Rank, x.Strides, out nint linearOffset, out RentedBuffer rentedBuffer); for (nint i = 0; i < x.FlattenedLength; i++) @@ -147,6 +237,23 @@ ref result public static void Invoke(in TensorSpan destination, TArg scalar) where TOperation : IUnaryOperation_Scalar { + if (destination.IsDense && destination.FlattenedLength <= int.MaxValue) + { + TOperation.Invoke(AsDenseSpan(destination), scalar); + return; + } + + if (GetDenseSliceDimension(destination._shape, destination._shape) is int dimension && dimension >= 0) + { + TensorDimensionSpan slices = destination.GetDimensionSpan(dimension); + for (nint i = 0; i < slices.Length; i++) + { + TensorSpan slice = slices[i]; + TOperation.Invoke(AsDenseSpan(slice), scalar); + } + return; + } + scoped Span indexes = RentedBuffer.Create(destination.Rank, destination.Strides, out nint linearOffset, out RentedBuffer rentedBuffer); for (nint i = 0; i < destination.FlattenedLength; i++) @@ -180,6 +287,29 @@ public static void Invoke(in ReadOnlyTensorSpan { return; } + ReadOnlyTensorSpan optimizedSource = x.Lengths.SequenceEqual(destination.Lengths) ? x : BroadcastSource(x, destination._shape); + if (destination.FlattenedLength != 0 && optimizedSource.Lengths.SequenceEqual(destination.Lengths)) + { + if (optimizedSource.IsDense && destination.IsDense && destination.FlattenedLength <= int.MaxValue) + { + TOperation.Invoke(AsDenseSpan(optimizedSource), AsDenseSpan(destination)); + return; + } + + int dimension = GetDenseSliceDimension(optimizedSource._shape, destination._shape); + if (dimension >= 0) + { + ReadOnlyTensorDimensionSpan sourceSlices = optimizedSource.GetDimensionSpan(dimension); + TensorDimensionSpan destinationSlices = destination.GetDimensionSpan(dimension); + for (nint i = 0; i < destinationSlices.Length; i++) + { + ReadOnlyTensorSpan sourceSlice = sourceSlices[i]; + TensorSpan destinationSlice = destinationSlices[i]; + TOperation.Invoke(AsDenseSpan(sourceSlice), AsDenseSpan(destinationSlice)); + } + return; + } + } { ReadOnlyTensorSpan source = x; scoped Span xIndexes = RentedBuffer.Create(destination.Rank, source.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); @@ -246,6 +376,24 @@ public static void Invoke(in ReadOnlyTensorSpan { ThrowHelper.ThrowArgument_OverlappingTensorLayoutsNotSupported(); } + + int dimension = x.FlattenedLength >= MinSlicedOperationLength + ? GetDenseSliceDimension(x._shape, x._shape) + : -1; + if (dimension >= 0) + { + ReadOnlyTensorDimensionSpan slices = x.GetDimensionSpan(dimension); + int destinationOffset = 0; + + for (nint i = 0; i < slices.Length; i++) + { + ReadOnlyTensorSpan slice = slices[i]; + ReadOnlySpan sourceSpan = AsDenseSpan(slice); + TOperation.Invoke(sourceSpan, result.Slice(destinationOffset, sourceSpan.Length)); + destinationOffset += sourceSpan.Length; + } + return; + } } scoped Span xIndexes = RentedBuffer.Create(x.Rank, x.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); @@ -288,6 +436,34 @@ public static void Invoke(in ReadOnlyTensorSp { ValidateSourceOverlap(x, destination); ValidateSourceOverlap(y, destination); + ReadOnlyTensorSpan optimizedX = x.Lengths.SequenceEqual(destination.Lengths) ? x : BroadcastSource(x, destination._shape); + ReadOnlyTensorSpan optimizedY = y.Lengths.SequenceEqual(destination.Lengths) ? y : BroadcastSource(y, destination._shape); + if (destination.FlattenedLength != 0 && + optimizedX.Lengths.SequenceEqual(destination.Lengths) && + optimizedY.Lengths.SequenceEqual(destination.Lengths)) + { + if (optimizedX.IsDense && optimizedY.IsDense && destination.IsDense && destination.FlattenedLength <= int.MaxValue) + { + TOperation.Invoke(AsDenseSpan(optimizedX), AsDenseSpan(optimizedY), AsDenseSpan(destination)); + return; + } + + int dimension = GetDenseSliceDimension(optimizedX._shape, destination._shape, optimizedY._shape); + if (dimension >= 0) + { + ReadOnlyTensorDimensionSpan xSlices = optimizedX.GetDimensionSpan(dimension); + ReadOnlyTensorDimensionSpan ySlices = optimizedY.GetDimensionSpan(dimension); + TensorDimensionSpan destinationSlices = destination.GetDimensionSpan(dimension); + for (nint i = 0; i < destinationSlices.Length; i++) + { + ReadOnlyTensorSpan xSlice = xSlices[i]; + ReadOnlyTensorSpan ySlice = ySlices[i]; + TensorSpan destinationSlice = destinationSlices[i]; + TOperation.Invoke(AsDenseSpan(xSlice), AsDenseSpan(ySlice), AsDenseSpan(destinationSlice)); + } + return; + } + } { ReadOnlyTensorSpan left = x; ReadOnlyTensorSpan right = y; @@ -352,6 +528,29 @@ public static void Invoke(in ReadOnlyTensorSp where TOperation : IBinaryOperation_Tensor_Scalar { ValidateSourceOverlap(x, destination); + ReadOnlyTensorSpan optimizedSource = x.Lengths.SequenceEqual(destination.Lengths) ? x : BroadcastSource(x, destination._shape); + if (destination.FlattenedLength != 0 && optimizedSource.Lengths.SequenceEqual(destination.Lengths)) + { + if (optimizedSource.IsDense && destination.IsDense && destination.FlattenedLength <= int.MaxValue) + { + TOperation.Invoke(AsDenseSpan(optimizedSource), y, AsDenseSpan(destination)); + return; + } + + int dimension = GetDenseSliceDimension(optimizedSource._shape, destination._shape); + if (dimension >= 0) + { + ReadOnlyTensorDimensionSpan sourceSlices = optimizedSource.GetDimensionSpan(dimension); + TensorDimensionSpan destinationSlices = destination.GetDimensionSpan(dimension); + for (nint i = 0; i < destinationSlices.Length; i++) + { + ReadOnlyTensorSpan sourceSlice = sourceSlices[i]; + TensorSpan destinationSlice = destinationSlices[i]; + TOperation.Invoke(AsDenseSpan(sourceSlice), y, AsDenseSpan(destinationSlice)); + } + return; + } + } { ReadOnlyTensorSpan source = x; scoped Span xIndexes = RentedBuffer.Create(destination.Rank, source.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); @@ -376,6 +575,29 @@ public static void Invoke(TArg x, in ReadOnlyTensorSp where TOperation : IBinaryOperation_Scalar_Tensor { ValidateSourceOverlap(y, destination); + ReadOnlyTensorSpan optimizedSource = y.Lengths.SequenceEqual(destination.Lengths) ? y : BroadcastSource(y, destination._shape); + if (destination.FlattenedLength != 0 && optimizedSource.Lengths.SequenceEqual(destination.Lengths)) + { + if (optimizedSource.IsDense && destination.IsDense && destination.FlattenedLength <= int.MaxValue) + { + TOperation.Invoke(x, AsDenseSpan(optimizedSource), AsDenseSpan(destination)); + return; + } + + int dimension = GetDenseSliceDimension(optimizedSource._shape, destination._shape); + if (dimension >= 0) + { + ReadOnlyTensorDimensionSpan sourceSlices = optimizedSource.GetDimensionSpan(dimension); + TensorDimensionSpan destinationSlices = destination.GetDimensionSpan(dimension); + for (nint i = 0; i < destinationSlices.Length; i++) + { + ReadOnlyTensorSpan sourceSlice = sourceSlices[i]; + TensorSpan destinationSlice = destinationSlices[i]; + TOperation.Invoke(x, AsDenseSpan(sourceSlice), AsDenseSpan(destinationSlice)); + } + return; + } + } { ReadOnlyTensorSpan source = y; scoped Span xIndexes = RentedBuffer.Create(destination.Rank, source.Strides, out nint xLinearOffset, out RentedBuffer xRentedBuffer); diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs index efbee736d4ea97..7c3e11c0e26681 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs @@ -152,11 +152,13 @@ private static nint CalculateTotalLength(ReadOnlySpan lengths) public static IEnumerable SpanInSpanOutData() { + const float TrigTolerance = 1e-4f; + yield return Create(float.Abs, Tensor.Abs); yield return Create(float.Acos, Tensor.Acos); yield return Create(float.Acosh, Tensor.Acosh); yield return Create(float.AcosPi, Tensor.AcosPi); - yield return Create(float.Asin, Tensor.Asin); + yield return Create(float.Asin, Tensor.Asin, TrigTolerance); yield return Create(float.Asinh, Tensor.Asinh); yield return Create(float.AsinPi, Tensor.AsinPi); yield return Create(float.Atan, Tensor.Atan); @@ -164,15 +166,15 @@ public static IEnumerable SpanInSpanOutData() yield return Create(float.AtanPi, Tensor.AtanPi); yield return Create(float.Cbrt, Tensor.Cbrt); yield return Create(float.Ceiling, Tensor.Ceiling); - yield return Create(float.Cos, Tensor.Cos); + yield return Create(float.Cos, Tensor.Cos, TrigTolerance); yield return Create(float.Cosh, Tensor.Cosh); - yield return Create(float.CosPi, Tensor.CosPi); + yield return Create(float.CosPi, Tensor.CosPi, TrigTolerance); yield return Create(float.DegreesToRadians, Tensor.DegreesToRadians); yield return Create(float.Exp, Tensor.Exp); - yield return Create(float.Exp10, Tensor.Exp10); - yield return Create(float.Exp10M1, Tensor.Exp10M1); - yield return Create(float.Exp2, Tensor.Exp2); - yield return Create(float.Exp2M1, Tensor.Exp2M1); + yield return Create(float.Exp10, Tensor.Exp10, 1e-5f); + yield return Create(float.Exp10M1, Tensor.Exp10M1, 1e-5f); + yield return Create(float.Exp2, Tensor.Exp2, 1e-5f); + yield return Create(float.Exp2M1, Tensor.Exp2M1, 1e-5f); yield return Create(float.ExpM1, Tensor.ExpM1); yield return Create(float.Floor, Tensor.Floor); yield return Create(int.LeadingZeroCount, Tensor.LeadingZeroCount); @@ -189,12 +191,12 @@ public static IEnumerable SpanInSpanOutData() yield return Create(f => 1 / f, Tensor.Reciprocal); yield return Create(float.Round, Tensor.Round); //yield return Create(float.Sigmoid, Tensor.Sigmoid); - yield return Create(float.Sin, Tensor.Sin); + yield return Create(float.Sin, Tensor.Sin, TrigTolerance); yield return Create(float.Sinh, Tensor.Sinh); - yield return Create(float.SinPi, Tensor.SinPi); + yield return Create(float.SinPi, Tensor.SinPi, TrigTolerance); //yield return Create(float.SoftMax, Tensor.SoftMax); yield return Create(float.Sqrt, Tensor.Sqrt); - yield return Create(float.Tan, Tensor.Tan); + yield return Create(float.Tan, Tensor.Tan, TrigTolerance); yield return Create(float.Tanh, Tensor.Tanh); yield return Create(float.TanPi, Tensor.TanPi); yield return Create(float.Truncate, Tensor.Truncate); @@ -203,14 +205,14 @@ public static IEnumerable SpanInSpanOutData() yield return Create(x => (int)x, Tensor.ConvertSaturating); yield return Create(x => (int)MathF.Truncate(x), Tensor.ConvertTruncating); - static object[] Create(TensorPrimitivesSpanInSpanOut tensorPrimitivesMethod, TensorSpanInSpanOut tensorOperation) - => new object[] { tensorPrimitivesMethod, tensorOperation }; + static object[] Create(TensorPrimitivesSpanInSpanOut tensorPrimitivesMethod, TensorSpanInSpanOut tensorOperation, float? tolerance = null) + => new object[] { tensorPrimitivesMethod, tensorOperation, tolerance }; } [Theory, MemberData(nameof(SpanInSpanOutData))] - public void TensorExtensionsSpanInSpanOut(TensorPrimitivesSpanInSpanOut tensorPrimitivesOperation, TensorSpanInSpanOut tensorOperation) + public void TensorExtensionsSpanInSpanOut(TensorPrimitivesSpanInSpanOut tensorPrimitivesOperation, TensorSpanInSpanOut tensorOperation, float? tolerance) where TIn : INumberBase - where TOut: INumber + where TOut : unmanaged, INumber { Assert.All(Helpers.TensorShapes, (tensorLength, index) => { @@ -232,7 +234,8 @@ public void TensorExtensionsSpanInSpanOut(TensorPrimitivesSpanInSpanO for (int i = 0; i < data.Length; i++) { - Assert.Equal(tensorPrimitivesOperation(data[i]), span[i]); + Helpers.AssertEqualWithTolerance(tensorPrimitivesOperation(data[i]), span[i], + tolerance.HasValue ? TOut.CreateTruncating(tolerance.Value) : null); } // Now test if the source is sliced to be smaller then the destination that the destination is also sliced @@ -257,7 +260,8 @@ public void TensorExtensionsSpanInSpanOut(TensorPrimitivesSpanInSpanO tensorResultsEnumMove = tensorResultsEnum.MoveNext(); Assert.True(tensorResultsEnumMove); - Assert.Equal(tensorPrimitivesOperation(sliceData[i]), tensorResultsEnum.Current); + Helpers.AssertEqualWithTolerance(tensorPrimitivesOperation(sliceData[i]), tensorResultsEnum.Current, + tolerance.HasValue ? TOut.CreateTruncating(tolerance.Value) : null); } }); } diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs index 417c6470a4c6f9..b8aba049c27916 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs @@ -41,11 +41,13 @@ private static nint CalculateTotalLength(ReadOnlySpan lengths) public static IEnumerable SpanInSpanOutData() { + const float TrigTolerance = 1e-4f; + yield return Create(float.Abs, Tensor.Abs); yield return Create(float.Acos, Tensor.Acos); yield return Create(float.Acosh, Tensor.Acosh); yield return Create(float.AcosPi, Tensor.AcosPi); - yield return Create(float.Asin, Tensor.Asin); + yield return Create(float.Asin, Tensor.Asin, TrigTolerance); yield return Create(float.Asinh, Tensor.Asinh); yield return Create(float.AsinPi, Tensor.AsinPi); yield return Create(float.Atan, Tensor.Atan); @@ -53,15 +55,15 @@ public static IEnumerable SpanInSpanOutData() yield return Create(float.AtanPi, Tensor.AtanPi); yield return Create(float.Cbrt, Tensor.Cbrt); yield return Create(float.Ceiling, Tensor.Ceiling); - yield return Create(float.Cos, Tensor.Cos); + yield return Create(float.Cos, Tensor.Cos, TrigTolerance); yield return Create(float.Cosh, Tensor.Cosh); - yield return Create(float.CosPi, Tensor.CosPi); + yield return Create(float.CosPi, Tensor.CosPi, TrigTolerance); yield return Create(float.DegreesToRadians, Tensor.DegreesToRadians); yield return Create(float.Exp, Tensor.Exp); - yield return Create(float.Exp10, Tensor.Exp10); - yield return Create(float.Exp10M1, Tensor.Exp10M1); - yield return Create(float.Exp2, Tensor.Exp2); - yield return Create(float.Exp2M1, Tensor.Exp2M1); + yield return Create(float.Exp10, Tensor.Exp10, 1e-5f); + yield return Create(float.Exp10M1, Tensor.Exp10M1, 1e-5f); + yield return Create(float.Exp2, Tensor.Exp2, 1e-5f); + yield return Create(float.Exp2M1, Tensor.Exp2M1, 1e-5f); yield return Create(float.ExpM1, Tensor.ExpM1); yield return Create(float.Floor, Tensor.Floor); yield return Create(int.LeadingZeroCount, Tensor.LeadingZeroCount); @@ -79,23 +81,23 @@ public static IEnumerable SpanInSpanOutData() yield return Create( f => 1 / f, Tensor.Reciprocal); yield return Create(float.Round, Tensor.Round); //yield return Create(float.Sigmoid, Tensor.Sigmoid); - yield return Create(float.Sin, Tensor.Sin); + yield return Create(float.Sin, Tensor.Sin, TrigTolerance); yield return Create(float.Sinh, Tensor.Sinh); - yield return Create(float.SinPi, Tensor.SinPi); + yield return Create(float.SinPi, Tensor.SinPi, TrigTolerance); //yield return Create(float.SoftMax, Tensor.SoftMax); yield return Create(float.Sqrt, Tensor.Sqrt); - yield return Create(float.Tan, Tensor.Tan); + yield return Create(float.Tan, Tensor.Tan, TrigTolerance); yield return Create(float.Tanh, Tensor.Tanh); yield return Create(float.TanPi, Tensor.TanPi); yield return Create(float.Truncate, Tensor.Truncate); - static object[] Create(PerformCalculationSpanInSpanOut tensorPrimitivesMethod, PerformSpanInSpanOut tensorOperation) - => new object[] { tensorPrimitivesMethod, tensorOperation }; + static object[] Create(PerformCalculationSpanInSpanOut tensorPrimitivesMethod, PerformSpanInSpanOut tensorOperation, float? tolerance = null) + => new object[] { tensorPrimitivesMethod, tensorOperation, tolerance }; } [Theory, MemberData(nameof(SpanInSpanOutData))] - public void TensorExtensionsSpanInSpanOut(PerformCalculationSpanInSpanOut tensorPrimitivesOperation, PerformSpanInSpanOut tensorOperation) - where T: INumberBase, IComparisonOperators + public void TensorExtensionsSpanInSpanOut(PerformCalculationSpanInSpanOut tensorPrimitivesOperation, PerformSpanInSpanOut tensorOperation, float? tolerance) + where T : unmanaged, INumber { Assert.All(Helpers.TensorShapes, tensorLength => { @@ -113,7 +115,8 @@ public void TensorExtensionsSpanInSpanOut(PerformCalculationSpanInSpanOut for (int i = 0; i < data.Length; i++) { - Assert.Equal(tensorPrimitivesOperation(data[i]), span[i]); + Helpers.AssertEqualWithTolerance(tensorPrimitivesOperation(data[i]), span[i], + tolerance.HasValue ? T.CreateTruncating(tolerance.Value) : null); } }); } @@ -838,6 +841,35 @@ public static void TensorSequenceEqualNonDenseTests(int[] data, nint[] shape, ni Assert.False(ts1.SequenceEqual(ts3)); } + [Theory] + [InlineData(4, 16)] + [InlineData(8, 8)] + [InlineData(2, 32)] + public static void TensorFlattenAndCompareContiguousRows(int rows, int columns) + { + int rowStride = columns + 2; + int[] backing = Enumerable.Repeat(-11, rows * rowStride).ToArray(); + int[] expected = Enumerable.Range(0, rows * columns).ToArray(); + nint[] lengths = [rows, columns]; + nint[] strides = [rowStride, 1]; + + for (int row = 0; row < rows; row++) + { + expected.AsSpan(row * columns, columns).CopyTo(backing.AsSpan(row * rowStride, columns)); + } + + ReadOnlyTensorSpan source = new ReadOnlyTensorSpan(backing, lengths, strides); + ReadOnlyTensorSpan dense = new ReadOnlyTensorSpan(expected, lengths); + int[] destination = Enumerable.Repeat(-7, expected.Length + 2).ToArray(); + source.FlattenTo(destination); + Assert.Equal(expected.Concat([-7, -7]), destination); + Assert.True(source.SequenceEqual(dense)); + Assert.True(dense.SequenceEqual(source)); + + backing[(rows - 1) * rowStride + columns - 1] = -99; + Assert.False(source.SequenceEqual(dense)); + } + /// /// Computes the set of buffer offsets that correspond to logical elements in a non-dense tensor. /// @@ -916,6 +948,38 @@ public static void TensorFillUniformDistributionNonDenseTests(int[] data, nint[] } } + [Theory] + [InlineData(false, 2, 4)] + [InlineData(false, 8, 16)] + [InlineData(true, 2, 4)] + [InlineData(true, 8, 16)] + public static void TensorFillNonDensePreservesLogicalRandomOrder(bool gaussian, int rows, int columns) + { + double[] backing = new double[rows * (columns + 3)]; + double[] expected = new double[rows * columns]; + TensorSpan strided = new TensorSpan(backing, [rows, columns], [columns + 3, 1]); + TensorSpan dense = new TensorSpan(expected, [rows, columns]); + + if (gaussian) + { + Tensor.FillGaussianNormalDistribution(strided, new Random(42)); + Tensor.FillGaussianNormalDistribution(dense, new Random(42)); + } + else + { + Tensor.FillUniformDistribution(strided, new Random(42)); + Tensor.FillUniformDistribution(dense, new Random(42)); + } + + double[] actual = new double[expected.Length]; + strided.FlattenTo(actual); + Assert.Equal(expected, actual); + for (int row = 0; row < rows; row++) + { + Assert.Equal([0.0, 0.0, 0.0], backing.AsSpan(row * (columns + 3) + columns, 3).ToArray()); + } + } + [Theory] [MemberData(nameof(NonDenseTensorData))] public static void TensorIndexOfMaxNonDenseTests(int[] data, nint[] shape, nint[] strides, int[] expectedLogical) @@ -1162,6 +1226,157 @@ public static void TensorResizeNonDenseTests() Assert.Equal(0, resized2[5]); } + [Theory] + [InlineData(31)] + [InlineData(33)] + [InlineData(64)] + [InlineData(130)] + public static void TensorResizeNonDenseContiguousRows(int newLength) + { + int[] backing = Enumerable.Repeat(-1, 8 * 19).ToArray(); + int[] expected = Enumerable.Range(1, 8 * 16).ToArray(); + for (int row = 0; row < 8; row++) + { + expected.AsSpan(row * 16, 16).CopyTo(backing.AsSpan(row * 19, 16)); + } + + Tensor source = Tensor.Create(backing, [8, 16], [19, 1]); + Tensor result = Tensor.Resize(source, [newLength]); + int[] actual = new int[newLength]; + result.FlattenTo(actual); + Assert.Equal(expected.Take(newLength).Concat(Enumerable.Repeat(0, Math.Max(0, newLength - expected.Length))), actual); + } + + [Theory] + [InlineData(8, 4, 1)] + [InlineData(12, 4, 1)] + [InlineData(13, 6, 1)] + [InlineData(16, 8, 2)] + public static void TensorOperationsOnContiguousSlices(int outerStride, int rowStride, int columnStride) + { + nint[] lengths = [2, 2, 4]; + nint[] strides = [outerStride, rowStride, columnStride]; + int[] xData = new int[32]; + int[] yData = new int[32]; + int[] resultData = Enumerable.Repeat(-99, 32).ToArray(); + int[] expected = Enumerable.Repeat(-99, 32).ToArray(); + bool[] comparisonData = new bool[32]; + bool[] expectedComparison = new bool[32]; + + for (int i = 0; i < 16; i++) + { + int offset = (i / 8 * outerStride) + (i / 4 % 2 * rowStride) + (i % 4 * columnStride); + xData[offset] = -i - 1; + yData[offset] = i + 1; + expectedComparison[offset] = true; + } + + ReadOnlyTensorSpan x = new ReadOnlyTensorSpan(xData, lengths, strides); + ReadOnlyTensorSpan y = new ReadOnlyTensorSpan(yData, lengths, strides); + TensorSpan result = new TensorSpan(resultData, lengths, strides); + TensorSpan comparison = new TensorSpan(comparisonData, lengths, strides); + + int[] flattened = Enumerable.Repeat(-99, 18).ToArray(); + x.FlattenTo(flattened); + Assert.Equal(Enumerable.Range(1, 16).Select(i => -i).Concat([-99, -99]), flattened); + + ReadOnlyTensorSpan dense = new ReadOnlyTensorSpan( + Enumerable.Range(1, 16).Select(i => -i).ToArray(), lengths); + Assert.True(x.SequenceEqual(dense)); + Assert.True(dense.SequenceEqual(x)); + Assert.False(y.SequenceEqual(dense)); + + int[] differing = (int[])xData.Clone(); + differing[outerStride + rowStride + 3 * columnStride] = -99; + Assert.False(x.SequenceEqual(new ReadOnlyTensorSpan(differing, lengths, strides))); + + Tensor.Abs(x, result); + for (int i = 0; i < 16; i++) + { + int offset = (i / 8 * outerStride) + (i / 4 % 2 * rowStride) + (i % 4 * columnStride); + expected[offset] = i + 1; + } + Assert.Equal(expected, resultData); + + Tensor.Add(x, y, result); + for (int i = 0; i < 16; i++) + { + int offset = (i / 8 * outerStride) + (i / 4 % 2 * rowStride) + (i % 4 * columnStride); + expected[offset] = 0; + } + Assert.Equal(expected, resultData); + + Tensor.Multiply(x, 2, result); + for (int i = 0; i < 16; i++) + { + int offset = (i / 8 * outerStride) + (i / 4 % 2 * rowStride) + (i % 4 * columnStride); + expected[offset] = -2 * (i + 1); + } + Assert.Equal(expected, resultData); + + Tensor.Subtract(50, x, result); + for (int i = 0; i < 16; i++) + { + int offset = (i / 8 * outerStride) + (i / 4 % 2 * rowStride) + (i % 4 * columnStride); + expected[offset] = 51 + i; + } + Assert.Equal(expected, resultData); + + Tensor.LessThan(x, y, comparison); + Assert.Equal(expectedComparison, comparisonData); + + Tensor.Multiply(result.AsReadOnlyTensorSpan(), 2, result); + for (int i = 0; i < 16; i++) + { + int offset = (i / 8 * outerStride) + (i / 4 % 2 * rowStride) + (i % 4 * columnStride); + expected[offset] *= 2; + } + Assert.Equal(expected, resultData); + + result.Clear(); + for (int i = 0; i < 16; i++) + { + int offset = (i / 8 * outerStride) + (i / 4 % 2 * rowStride) + (i % 4 * columnStride); + expected[offset] = 0; + } + Assert.Equal(expected, resultData); + + result.Fill(7); + for (int i = 0; i < 16; i++) + { + int offset = (i / 8 * outerStride) + (i / 4 % 2 * rowStride) + (i % 4 * columnStride); + expected[offset] = 7; + } + Assert.Equal(expected, resultData); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void TensorOperationsOnBroadcastContiguousSlices(bool paddedRank) + { + int[] sourceData = [-1, -2, -3, -4]; + ReadOnlyTensorSpan source = paddedRank + ? new ReadOnlyTensorSpan(sourceData, [1, 4]) + : new ReadOnlyTensorSpan(sourceData, [4]); + ReadOnlyTensorSpan other = new ReadOnlyTensorSpan( + [10, 11, 12, 13, 14, 15, 16, 17], [2, 4]); + int[] resultData = Enumerable.Repeat(-99, 12).ToArray(); + TensorSpan result = new TensorSpan(resultData, [2, 4], [6, 1]); + + Tensor.Abs(source, result); + Assert.Equal([1, 2, 3, 4, -99, -99, 1, 2, 3, 4, -99, -99], resultData); + + Tensor.Add(source, other, result); + Assert.Equal([9, 9, 9, 9, -99, -99, 13, 13, 13, 13, -99, -99], resultData); + + Tensor.Add(source, 3, result); + Assert.Equal([2, 1, 0, -1, -99, -99, 2, 1, 0, -1, -99, -99], resultData); + + Tensor.Subtract(10, source, result); + Assert.Equal([11, 12, 13, 14, -99, -99, 11, 12, 13, 14, -99, -99], resultData); + } + [Fact] public static void TensorMultiplyTests() { From a115e5ecdde10e92047c3edca28e6e2764576b78 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Tue, 29 Sep 2026 22:17:26 -0700 Subject: [PATCH 3/6] Fix Tensor AOT validation and pooled buffer lifetimes Reject nonexistent axes for rank-zero permutations and return temporary buffers on exceptional paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../System/Numerics/Tensors/netcore/Tensor.cs | 222 ++++++++++++------ .../src/System/ThrowHelper.cs | 54 ++++- .../tests/TensorSpanTests.cs | 49 ++++ .../tests/TensorTests.cs | 36 +++ 4 files changed, 280 insertions(+), 81 deletions(-) diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs index fed118595b789d..ecf45efaf1f6c1 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs @@ -1400,7 +1400,7 @@ public static bool LessThanOrEqualAny(T x, in ReadOnlyTensorSpan y) /// with the new axis ordering. public static Tensor PermuteDimensions(this Tensor tensor, ReadOnlySpan dimensions) { - if (tensor.Rank <= 1 && (dimensions.IsEmpty || (dimensions.Length == 1 && dimensions[0] == 0))) + if (tensor.Rank <= 1 && (dimensions.IsEmpty || (tensor.Rank == 1 && dimensions.Length == 1 && dimensions[0] == 0))) { return tensor; } @@ -1412,49 +1412,62 @@ public static Tensor PermuteDimensions(this Tensor tensor, ReadOnlySpan } scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer lengthsRentedBuffer); - scoped Span newStrides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer stridesRentedBuffer); - - Tensor outTensor; - - if (dimensions.IsEmpty) + try { - for (int i = 0; i < tensor.Rank; i++) + scoped Span newStrides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer stridesRentedBuffer); + try { - newLengths[i] = tensor.Lengths[tensor.Rank - 1 - i]; - newStrides[i] = tensor.Strides[tensor.Rank - 1 - i]; - } - } - else - { - scoped Span seen = TensorOperation.RentedBuffer.CreateUninitialized( - (tensor.Rank - 1) / 64 + 1, out TensorOperation.RentedBuffer seenRentedBuffer); - for (int i = 0; i < dimensions.Length; i++) - { - int dimension = dimensions[i]; - if ((uint)dimension >= (uint)tensor.Lengths.Length) + if (dimensions.IsEmpty) { - ThrowHelper.ThrowArgument_InvalidDimension(); + for (int i = 0; i < tensor.Rank; i++) + { + newLengths[i] = tensor.Lengths[tensor.Rank - 1 - i]; + newStrides[i] = tensor.Strides[tensor.Rank - 1 - i]; + } } - - int word = dimension / 64; - ulong bit = 1UL << (dimension % 64); - if ((seen[word] & bit) != 0) + else { - ThrowHelper.ThrowArgument_PermuteAxisOrder(); + scoped Span seen = TensorOperation.RentedBuffer.CreateUninitialized( + (tensor.Rank - 1) / 64 + 1, out TensorOperation.RentedBuffer seenRentedBuffer); + try + { + for (int i = 0; i < dimensions.Length; i++) + { + int dimension = dimensions[i]; + if ((uint)dimension >= (uint)tensor.Lengths.Length) + { + ThrowHelper.ThrowArgument_InvalidDimension(); + } + + int word = dimension / 64; + ulong bit = 1UL << (dimension % 64); + if ((seen[word] & bit) != 0) + { + ThrowHelper.ThrowArgument_PermuteAxisOrder(); + } + + seen[word] |= bit; + newLengths[i] = tensor.Lengths[dimension]; + newStrides[i] = tensor.Strides[dimension]; + } + } + finally + { + seenRentedBuffer.Dispose(); + } } - seen[word] |= bit; - newLengths[i] = tensor.Lengths[dimension]; - newStrides[i] = tensor.Strides[dimension]; + return new Tensor(tensor._values, tensor._start, newLengths, newStrides, tensor.IsPinned); + } + finally + { + stridesRentedBuffer.Dispose(); } - seenRentedBuffer.Dispose(); } - outTensor = new Tensor(tensor._values, tensor._start, newLengths, newStrides, tensor.IsPinned); - - lengthsRentedBuffer.Dispose(); - stridesRentedBuffer.Dispose(); - - return outTensor; + finally + { + lengthsRentedBuffer.Dispose(); + } } } #endregion @@ -1510,19 +1523,17 @@ private static void GetReshapeLengths(ReadOnlySpan lengths, nint flattened } - private static Span GetReshapeStrides(ReadOnlySpan sourceLengths, ReadOnlySpan sourceStrides, - bool isDense, ReadOnlySpan lengths, [UnscopedRef] out TensorOperation.RentedBuffer rentedBuffer) + private static void GetReshapeStrides(ReadOnlySpan sourceLengths, ReadOnlySpan sourceStrides, + bool isDense, ReadOnlySpan lengths, Span strides) { if (isDense) { - rentedBuffer = default; - return []; + return; } - Span strides = TensorOperation.RentedBuffer.CreateUninitialized(lengths.Length, out rentedBuffer); if (!sourceStrides.ContainsAnyExcept(0)) { - return strides; + return; } int sourceIndex = 0; @@ -1551,7 +1562,6 @@ private static Span GetReshapeStrides(ReadOnlySpan sourceLengths, Re { ThrowHelper.ThrowArgument_InvalidReshapeDimensions(); } - return strides; } /// @@ -1574,13 +1584,25 @@ public static Tensor Reshape(this Tensor tensor, ReadOnlySpan len } scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(lengths.Length, out TensorOperation.RentedBuffer lengthsRentedBuffer); - GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); - scoped Span strides = GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, out TensorOperation.RentedBuffer stridesRentedBuffer); + try + { + GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); + scoped Span strides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.IsDense ? 0 : newLengths.Length, out TensorOperation.RentedBuffer stridesRentedBuffer); + try + { + GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, strides); - Tensor result = new Tensor(tensor._values, tensor._start, newLengths, strides, tensor.IsPinned); - stridesRentedBuffer.Dispose(); - lengthsRentedBuffer.Dispose(); - return result; + return new Tensor(tensor._values, tensor._start, newLengths, strides, tensor.IsPinned); + } + finally + { + stridesRentedBuffer.Dispose(); + } + } + finally + { + lengthsRentedBuffer.Dispose(); + } } /// @@ -1603,13 +1625,25 @@ public static TensorSpan Reshape(this scoped in TensorSpan tensor, scop } scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(lengths.Length, out TensorOperation.RentedBuffer lengthsRentedBuffer); - GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); - scoped Span strides = GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, out TensorOperation.RentedBuffer stridesRentedBuffer); + try + { + GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); + scoped Span strides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.IsDense ? 0 : newLengths.Length, out TensorOperation.RentedBuffer stridesRentedBuffer); + try + { + GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, strides); - TensorSpan output = new TensorSpan(ref tensor._reference, tensor._shape.LinearLength, newLengths, strides, tensor.IsPinned); - stridesRentedBuffer.Dispose(); - lengthsRentedBuffer.Dispose(); - return output; + return new TensorSpan(ref tensor._reference, tensor._shape.LinearLength, newLengths, strides, tensor.IsPinned); + } + finally + { + stridesRentedBuffer.Dispose(); + } + } + finally + { + lengthsRentedBuffer.Dispose(); + } } /// @@ -1632,13 +1666,25 @@ public static ReadOnlyTensorSpan Reshape(this scoped in ReadOnlyTensorSpan } scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(lengths.Length, out TensorOperation.RentedBuffer lengthsRentedBuffer); - GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); - scoped Span strides = GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, out TensorOperation.RentedBuffer stridesRentedBuffer); + try + { + GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); + scoped Span strides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.IsDense ? 0 : newLengths.Length, out TensorOperation.RentedBuffer stridesRentedBuffer); + try + { + GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, strides); - ReadOnlyTensorSpan output = new ReadOnlyTensorSpan(ref tensor._reference, tensor._shape.LinearLength, newLengths, strides, tensor.IsPinned); - stridesRentedBuffer.Dispose(); - lengthsRentedBuffer.Dispose(); - return output; + return new ReadOnlyTensorSpan(ref tensor._reference, tensor._shape.LinearLength, newLengths, strides, tensor.IsPinned); + } + finally + { + stridesRentedBuffer.Dispose(); + } + } + finally + { + lengthsRentedBuffer.Dispose(); + } } #endregion @@ -3874,10 +3920,16 @@ private static nint IndexOfMaxFallback(scoped in ReadOnlyTensorSpan x) where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - x.FlattenTo(flat); - nint result = TensorPrimitives.IndexOfMax(flat.AsSpan(0, (int)x.FlattenedLength)); - ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); - return result; + try + { + x.FlattenTo(flat); + + return TensorPrimitives.IndexOfMax(flat.AsSpan(0, (int)x.FlattenedLength)); + } + finally + { + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + } } #endregion @@ -3901,10 +3953,16 @@ private static nint IndexOfMaxMagnitudeFallback(scoped in ReadOnlyTensorSpan< where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - x.FlattenTo(flat); - nint result = TensorPrimitives.IndexOfMaxMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); - ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); - return result; + try + { + x.FlattenTo(flat); + + return TensorPrimitives.IndexOfMaxMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); + } + finally + { + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + } } #endregion @@ -3927,10 +3985,16 @@ private static nint IndexOfMinFallback(scoped in ReadOnlyTensorSpan x) where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - x.FlattenTo(flat); - nint result = TensorPrimitives.IndexOfMin(flat.AsSpan(0, (int)x.FlattenedLength)); - ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); - return result; + try + { + x.FlattenTo(flat); + + return TensorPrimitives.IndexOfMin(flat.AsSpan(0, (int)x.FlattenedLength)); + } + finally + { + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + } } #endregion @@ -3955,10 +4019,16 @@ private static nint IndexOfMinMagnitudeFallback(scoped in ReadOnlyTensorSpan< where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - x.FlattenTo(flat); - nint result = TensorPrimitives.IndexOfMinMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); - ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); - return result; + try + { + x.FlattenTo(flat); + + return TensorPrimitives.IndexOfMinMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); + } + finally + { + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + } } #endregion diff --git a/src/libraries/System.Numerics.Tensors/src/System/ThrowHelper.cs b/src/libraries/System.Numerics.Tensors/src/System/ThrowHelper.cs index b15e33b684e9b2..d9673d7c6efb9d 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/ThrowHelper.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/ThrowHelper.cs @@ -38,16 +38,60 @@ public static void ThrowIfArrayTypeMismatch(Array? array, bool isReadOnly) return; } - Type arrayType = array.GetType(); - if (arrayType.GetElementType() == typeof(T)) + Type elementType = array.GetType().GetElementType()!; + if (elementType == typeof(T)) { return; } - if ((array is not T[] && !typeof(T).MakeArrayType(array.Rank).IsInstanceOfType(array)) || - (!isReadOnly && !typeof(T).IsValueType)) + if (elementType.IsValueType) { - ThrowArrayTypeMismatchException(); + // Array assignments allow enums and integers with the same reduced type. + if (ReducedType(elementType) == ReducedType(typeof(T))) + { + return; + } + } + else if (isReadOnly && typeof(T).IsAssignableFrom(elementType)) + { + return; + } + + ThrowArrayTypeMismatchException(); + + static Type ReducedType(Type type) + { + if (type.IsEnum) + { + type = Enum.GetUnderlyingType(type); + } + + if (type == typeof(byte)) + { + return typeof(sbyte); + } + + if (type == typeof(ushort)) + { + return typeof(short); + } + + if (type == typeof(uint)) + { + return typeof(int); + } + + if (type == typeof(ulong)) + { + return typeof(long); + } + + if (type == typeof(nuint) || type == typeof(nint)) + { + return IntPtr.Size == 8 ? typeof(long) : typeof(int); + } + + return type; } } diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs index 7c3e11c0e26681..8ebc825b568e55 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs @@ -996,6 +996,55 @@ public static void TensorSpanArrayConstructorSupportsCompatibleValueTypeArrays() Assert.Throws(() => new TensorSpan((Array)signed2D, [0, 0], [1, 2], [])); } + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public static void TensorSpanArrayConstructorPreservesElementCompatibility(int rank) + { + Validate(CreateArray(rank), true); + Validate(CreateArray(rank), true); + Validate(CreateArray(rank), true); + Validate(CreateArray(rank), true); + Validate(CreateArray(rank), true); + Validate(CreateArray(rank), true); + Validate(CreateArray(rank), true); + Validate(IntPtr.Size == 8 ? CreateArray(rank) : CreateArray(rank), true); + Validate(CreateArray(rank), false); + Validate(CreateArray(rank), false); + Validate(CreateArray(rank), false); + Validate(CreateArray(rank), false); + + static Array CreateArray(int rank) => rank switch + { + 1 => new TElement[1], + 2 => new TElement[1, 1], + _ => new TElement[1, 1, 1], + }; + + static void Validate(Array array, bool compatible) + { + int[] start = new int[array.Rank]; + nint[] lengths = new nint[array.Rank]; + Array.Fill(lengths, 1); + + if (compatible) + { + Assert.Equal(1, new TensorSpan(array).FlattenedLength); + Assert.Equal(1, new TensorSpan(array, start, lengths, []).FlattenedLength); + Assert.Equal(1, new ReadOnlyTensorSpan(array).FlattenedLength); + Assert.Equal(1, new ReadOnlyTensorSpan(array, start, lengths, []).FlattenedLength); + } + else + { + Assert.Throws(() => new TensorSpan(array)); + Assert.Throws(() => new TensorSpan(array, start, lengths, [])); + Assert.Throws(() => new ReadOnlyTensorSpan(array)); + Assert.Throws(() => new ReadOnlyTensorSpan(array, start, lengths, [])); + } + } + } + [Fact] public static void TensorSpanSpanConstructorTests() { diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs index b8aba049c27916..4226418e40d80a 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs @@ -1685,6 +1685,16 @@ public static void TensorRejectsDuplicatePermutationAxes() Assert.Throws(() => Tensor.Create([42]).PermuteDimensions([1])); } + [Fact] + public static void TensorRankZeroPermutationRequiresEmptyDimensions() + { + Tensor tensor = Tensor.Empty; + Assert.Same(tensor, tensor.PermuteDimensions([])); + Assert.Throws(() => tensor.PermuteDimensions([0])); + Assert.Throws(() => tensor.PermuteDimensions([-1])); + Assert.Throws(() => tensor.PermuteDimensions([0, 1])); + } + [Theory] [InlineData(2)] [InlineData(3)] @@ -1708,6 +1718,12 @@ public static void TensorPermuteDimensionsAcrossBufferSizes(int rank) dimensions[0] = dimensions[^1]; Assert.Throws(() => tensor.PermuteDimensions(dimensions)); + + dimensions[0] = rank; + Assert.Throws(() => tensor.PermuteDimensions(dimensions)); + + dimensions[0] = rank - 1; + Assert.Equal([1, 2], tensor.PermuteDimensions(dimensions).ToArray()); } [Theory] @@ -3753,6 +3769,26 @@ public static void TensorReshapeHandlesZeroDimensions() Assert.Equal([1, 2, 3, 4], tensor.Reshape([1, 4]).Reshape([4]).ToArray()); } + [Theory] + [InlineData(4, false)] + [InlineData(4, true)] + [InlineData(6, false)] + [InlineData(6, true)] + public static void TensorReshapeRejectsInvalidShapeAcrossBufferSizes(int rank, bool isBroadcast) + { + Tensor tensor = isBroadcast + ? Tensor.Create([1, 2], [2, 2], [0, 1]) + : Tensor.Create([1, 2, 3, 4]); + nint[] invalidLengths = new nint[rank]; + Array.Fill(invalidLengths, (nint)1); + invalidLengths[^1] = isBroadcast ? 4 : -2; + + Assert.Throws(() => tensor.Reshape(invalidLengths)); + Assert.Throws(() => tensor.AsTensorSpan().Reshape(invalidLengths)); + Assert.Throws(() => tensor.AsReadOnlyTensorSpan().Reshape(invalidLengths)); + Assert.Equal(isBroadcast ? [1, 2, 1, 2] : [1, 2, 3, 4], tensor.ToArray()); + } + [Theory] [InlineData(4, false)] [InlineData(4, true)] From 2d7e160bc16547179ac00d039219684d4febc9ec Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Tue, 29 Sep 2026 23:41:34 -0700 Subject: [PATCH 4/6] Fix Tensor empty reshapes and sliced dispatch thresholds Keep normal-path buffer returns without exception-path finally blocks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../System/Numerics/Tensors/netcore/Tensor.cs | 215 +++++++----------- .../Tensors/netcore/TensorOperation.cs | 14 +- .../tests/TensorSpanTests.cs | 50 ++++ .../tests/TensorTests.cs | 135 +++++++++++ 4 files changed, 271 insertions(+), 143 deletions(-) diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs index ecf45efaf1f6c1..281a3496bcd78f 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/Tensor.cs @@ -1412,62 +1412,46 @@ public static Tensor PermuteDimensions(this Tensor tensor, ReadOnlySpan } scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer lengthsRentedBuffer); - try + scoped Span newStrides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer stridesRentedBuffer); + if (dimensions.IsEmpty) { - scoped Span newStrides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.Rank, out TensorOperation.RentedBuffer stridesRentedBuffer); - try + for (int i = 0; i < tensor.Rank; i++) { - if (dimensions.IsEmpty) + newLengths[i] = tensor.Lengths[tensor.Rank - 1 - i]; + newStrides[i] = tensor.Strides[tensor.Rank - 1 - i]; + } + } + else + { + scoped Span seen = TensorOperation.RentedBuffer.CreateUninitialized( + (tensor.Rank - 1) / 64 + 1, out TensorOperation.RentedBuffer seenRentedBuffer); + for (int i = 0; i < dimensions.Length; i++) + { + int dimension = dimensions[i]; + if ((uint)dimension >= (uint)tensor.Lengths.Length) { - for (int i = 0; i < tensor.Rank; i++) - { - newLengths[i] = tensor.Lengths[tensor.Rank - 1 - i]; - newStrides[i] = tensor.Strides[tensor.Rank - 1 - i]; - } + ThrowHelper.ThrowArgument_InvalidDimension(); } - else + + int word = dimension / 64; + ulong bit = 1UL << (dimension % 64); + if ((seen[word] & bit) != 0) { - scoped Span seen = TensorOperation.RentedBuffer.CreateUninitialized( - (tensor.Rank - 1) / 64 + 1, out TensorOperation.RentedBuffer seenRentedBuffer); - try - { - for (int i = 0; i < dimensions.Length; i++) - { - int dimension = dimensions[i]; - if ((uint)dimension >= (uint)tensor.Lengths.Length) - { - ThrowHelper.ThrowArgument_InvalidDimension(); - } - - int word = dimension / 64; - ulong bit = 1UL << (dimension % 64); - if ((seen[word] & bit) != 0) - { - ThrowHelper.ThrowArgument_PermuteAxisOrder(); - } - - seen[word] |= bit; - newLengths[i] = tensor.Lengths[dimension]; - newStrides[i] = tensor.Strides[dimension]; - } - } - finally - { - seenRentedBuffer.Dispose(); - } + ThrowHelper.ThrowArgument_PermuteAxisOrder(); } - return new Tensor(tensor._values, tensor._start, newLengths, newStrides, tensor.IsPinned); - } - finally - { - stridesRentedBuffer.Dispose(); + seen[word] |= bit; + newLengths[i] = tensor.Lengths[dimension]; + newStrides[i] = tensor.Strides[dimension]; } + seenRentedBuffer.Dispose(); } - finally - { - lengthsRentedBuffer.Dispose(); - } + + Tensor result = new Tensor(tensor._values, tensor._start, newLengths, newStrides, tensor.IsPinned); + stridesRentedBuffer.Dispose(); + lengthsRentedBuffer.Dispose(); + + return result; } } #endregion @@ -1486,7 +1470,8 @@ private static void GetReshapeLengths(ReadOnlySpan lengths, nint flattened lengths.CopyTo(result); int wildcardIndex = -1; - nint knownProduct = 1; + // A zero makes the entire product zero, regardless of preceding dimension sizes. + nint knownProduct = lengths.Contains(0) ? 0 : 1; for (int i = 0; i < lengths.Length; i++) { nint length = lengths[i]; @@ -1584,25 +1569,15 @@ public static Tensor Reshape(this Tensor tensor, ReadOnlySpan len } scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(lengths.Length, out TensorOperation.RentedBuffer lengthsRentedBuffer); - try - { - GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); - scoped Span strides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.IsDense ? 0 : newLengths.Length, out TensorOperation.RentedBuffer stridesRentedBuffer); - try - { - GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, strides); + GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); + scoped Span strides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.IsDense ? 0 : newLengths.Length, out TensorOperation.RentedBuffer stridesRentedBuffer); + GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, strides); - return new Tensor(tensor._values, tensor._start, newLengths, strides, tensor.IsPinned); - } - finally - { - stridesRentedBuffer.Dispose(); - } - } - finally - { - lengthsRentedBuffer.Dispose(); - } + Tensor result = new Tensor(tensor._values, tensor._start, newLengths, strides, tensor.IsPinned); + stridesRentedBuffer.Dispose(); + lengthsRentedBuffer.Dispose(); + + return result; } /// @@ -1625,25 +1600,15 @@ public static TensorSpan Reshape(this scoped in TensorSpan tensor, scop } scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(lengths.Length, out TensorOperation.RentedBuffer lengthsRentedBuffer); - try - { - GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); - scoped Span strides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.IsDense ? 0 : newLengths.Length, out TensorOperation.RentedBuffer stridesRentedBuffer); - try - { - GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, strides); + GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); + scoped Span strides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.IsDense ? 0 : newLengths.Length, out TensorOperation.RentedBuffer stridesRentedBuffer); + GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, strides); - return new TensorSpan(ref tensor._reference, tensor._shape.LinearLength, newLengths, strides, tensor.IsPinned); - } - finally - { - stridesRentedBuffer.Dispose(); - } - } - finally - { - lengthsRentedBuffer.Dispose(); - } + TensorSpan result = new TensorSpan(ref tensor._reference, tensor._shape.LinearLength, newLengths, strides, tensor.IsPinned); + stridesRentedBuffer.Dispose(); + lengthsRentedBuffer.Dispose(); + + return result; } /// @@ -1666,25 +1631,15 @@ public static ReadOnlyTensorSpan Reshape(this scoped in ReadOnlyTensorSpan } scoped Span newLengths = TensorOperation.RentedBuffer.CreateUninitialized(lengths.Length, out TensorOperation.RentedBuffer lengthsRentedBuffer); - try - { - GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); - scoped Span strides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.IsDense ? 0 : newLengths.Length, out TensorOperation.RentedBuffer stridesRentedBuffer); - try - { - GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, strides); + GetReshapeLengths(lengths, tensor.FlattenedLength, newLengths); + scoped Span strides = TensorOperation.RentedBuffer.CreateUninitialized(tensor.IsDense ? 0 : newLengths.Length, out TensorOperation.RentedBuffer stridesRentedBuffer); + GetReshapeStrides(tensor.Lengths, tensor.Strides, tensor.IsDense, newLengths, strides); - return new ReadOnlyTensorSpan(ref tensor._reference, tensor._shape.LinearLength, newLengths, strides, tensor.IsPinned); - } - finally - { - stridesRentedBuffer.Dispose(); - } - } - finally - { - lengthsRentedBuffer.Dispose(); - } + ReadOnlyTensorSpan result = new ReadOnlyTensorSpan(ref tensor._reference, tensor._shape.LinearLength, newLengths, strides, tensor.IsPinned); + stridesRentedBuffer.Dispose(); + lengthsRentedBuffer.Dispose(); + + return result; } #endregion @@ -3920,16 +3875,12 @@ private static nint IndexOfMaxFallback(scoped in ReadOnlyTensorSpan x) where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - try - { - x.FlattenTo(flat); + x.FlattenTo(flat); - return TensorPrimitives.IndexOfMax(flat.AsSpan(0, (int)x.FlattenedLength)); - } - finally - { - ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); - } + nint result = TensorPrimitives.IndexOfMax(flat.AsSpan(0, (int)x.FlattenedLength)); + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + + return result; } #endregion @@ -3953,16 +3904,12 @@ private static nint IndexOfMaxMagnitudeFallback(scoped in ReadOnlyTensorSpan< where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - try - { - x.FlattenTo(flat); + x.FlattenTo(flat); - return TensorPrimitives.IndexOfMaxMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); - } - finally - { - ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); - } + nint result = TensorPrimitives.IndexOfMaxMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + + return result; } #endregion @@ -3985,16 +3932,12 @@ private static nint IndexOfMinFallback(scoped in ReadOnlyTensorSpan x) where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - try - { - x.FlattenTo(flat); + x.FlattenTo(flat); - return TensorPrimitives.IndexOfMin(flat.AsSpan(0, (int)x.FlattenedLength)); - } - finally - { - ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); - } + nint result = TensorPrimitives.IndexOfMin(flat.AsSpan(0, (int)x.FlattenedLength)); + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + + return result; } #endregion @@ -4019,16 +3962,12 @@ private static nint IndexOfMinMagnitudeFallback(scoped in ReadOnlyTensorSpan< where T : INumber { T[] flat = ArrayPool.Shared.Rent((int)x.FlattenedLength); - try - { - x.FlattenTo(flat); + x.FlattenTo(flat); - return TensorPrimitives.IndexOfMinMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); - } - finally - { - ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); - } + nint result = TensorPrimitives.IndexOfMinMagnitude(flat.AsSpan(0, (int)x.FlattenedLength)); + ArrayPool.Shared.Return(flat, RuntimeHelpers.IsReferenceOrContainsReferences()); + + return result; } #endregion diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs index 187141d8156902..ffadb4a4bd8a10 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorOperation.cs @@ -14,6 +14,9 @@ internal static class TensorOperation // Constructing slices costs more than indexed iteration for tiny tensors. internal const int MinSlicedOperationLength = 32; + // Three indexed traversals make binary slicing pay off at 16 elements rather than 32. + private const int MinSlicedBinaryOperationLength = 16; + private static bool ValidateSourceOverlap( in ReadOnlyTensorSpan source, in TensorSpan destination, bool isCopy = false) { @@ -77,12 +80,13 @@ private static ReadOnlyTensorSpan BroadcastSource(in ReadOnlyTensorSpan return new ReadOnlyTensorSpan(in source._reference, in shape); } - internal static int GetDenseSliceDimension(in TensorShape source, in TensorShape destination, in TensorShape other = default) + internal static int GetDenseSliceDimension(in TensorShape source, in TensorShape destination, in TensorShape other = default, + int minimumLength = MinSlicedOperationLength) { ReadOnlySpan lengths = destination.Lengths; - if (!source.Lengths.SequenceEqual(lengths) || - (other.Rank != 0 && !other.Lengths.SequenceEqual(lengths)) || - destination.FlattenedLength == 0) + if (destination.FlattenedLength < minimumLength || + !source.Lengths.SequenceEqual(lengths) || + (other.Rank != 0 && !other.Lengths.SequenceEqual(lengths))) { return -1; } @@ -448,7 +452,7 @@ public static void Invoke(in ReadOnlyTensorSp return; } - int dimension = GetDenseSliceDimension(optimizedX._shape, destination._shape, optimizedY._shape); + int dimension = GetDenseSliceDimension(optimizedX._shape, destination._shape, optimizedY._shape, MinSlicedBinaryOperationLength); if (dimension >= 0) { ReadOnlyTensorDimensionSpan xSlices = optimizedX.GetDimensionSpan(dimension); diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs index 8ebc825b568e55..151bc4f127bc36 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorSpanTests.cs @@ -26,6 +26,56 @@ public static void InvalidShapeAndSliceThrowDocumentedExceptions() new TensorSpan(data, [4, 4], [])[5, 0]); } + [Theory] + [InlineData(3, 0)] + [InlineData(3, 1)] + [InlineData(3, 2)] + [InlineData(3, 3)] + [InlineData(6, 0)] + [InlineData(6, 1)] + [InlineData(6, 2)] + [InlineData(6, 3)] + public static void InvalidExplicitShapeAcrossBufferSizes(int rank, int invalidKind) + { + nint[] lengths = new nint[rank]; + nint[] strides = new nint[rank]; + Array.Fill(lengths, (nint)1); + lengths[0] = 2; + lengths[1] = 2; + strides[0] = 2; + strides[1] = 1; + switch (invalidKind) + { + case 0: + lengths[0] = -2; + break; + case 1: + strides[0] = -2; + break; + case 2: + strides[0] = 1; + break; + case 3: + strides[0] = nint.MaxValue; + break; + } + + int[] data = [1, 2, 3, 4]; + if (invalidKind != 2) + { + Assert.Throws(() => Tensor.Create(data, lengths, strides)); + Assert.Throws(() => new TensorSpan(data, lengths, strides)); + Assert.Throws(() => new ReadOnlyTensorSpan(data, lengths, strides)); + } + else + { + Assert.Throws(() => Tensor.Create(data, lengths, strides)); + Assert.Throws(() => new TensorSpan(data, lengths, strides)); + Assert.Throws(() => new ReadOnlyTensorSpan(data, lengths, strides)); + } + Assert.Equal([1, 2, 3, 4], data); + } + [Fact] public static void ReshapeHandlesZeroDimensions() { diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs index 4226418e40d80a..da21a2dcd981f6 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs @@ -1350,6 +1350,60 @@ public static void TensorOperationsOnContiguousSlices(int outerStride, int rowSt Assert.Equal(expected, resultData); } + [Theory] + [InlineData(2)] + [InlineData(7)] + [InlineData(8)] + [InlineData(9)] + [InlineData(15)] + [InlineData(16)] + [InlineData(17)] + [InlineData(64)] + public static void TensorGappedOperationsAcrossSliceThreshold(int columns) + { + int rowStride = columns + 3; + nint[] lengths = [2, columns]; + nint[] strides = [rowStride, 1]; + int[] input = Enumerable.Repeat(-99, 2 * rowStride).ToArray(); + int[] output = Enumerable.Repeat(-99, input.Length).ToArray(); + int[] expected = (int[])output.Clone(); + for (int row = 0; row < 2; row++) + { + for (int column = 0; column < columns; column++) + { + input[row * rowStride + column] = -(row * columns + column + 1); + } + } + + ReadOnlyTensorSpan source = new ReadOnlyTensorSpan(input, lengths, strides); + TensorSpan destination = new TensorSpan(output, lengths, strides); + + Tensor.Abs(source, destination); + Validate(1, 0); + Tensor.Add(source, source, destination); + Validate(-2, 0); + Tensor.Multiply(source, 3, destination); + Validate(-3, 0); + Tensor.Subtract(50, source, destination); + Validate(1, 50); + destination.Clear(); + Validate(0, 0); + destination.Fill(7); + Validate(0, 7); + + void Validate(int multiplier, int addend) + { + for (int row = 0; row < 2; row++) + { + for (int column = 0; column < columns; column++) + { + expected[row * rowStride + column] = multiplier * (row * columns + column + 1) + addend; + } + } + Assert.Equal(expected, output); + } + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -1377,6 +1431,49 @@ public static void TensorOperationsOnBroadcastContiguousSlices(bool paddedRank) Assert.Equal([11, 12, 13, 14, -99, -99, 11, 12, 13, 14, -99, -99], resultData); } + [Theory] + [InlineData(3, 2)] + [InlineData(6, 2)] + [InlineData(6, 8)] + [InlineData(6, 16)] + public static void TensorGappedOperationsPropagateExceptions(int rank, int columns) + { + nint[] lengths = Enumerable.Repeat((nint)1, rank).ToArray(); + nint[] strides = new nint[rank]; + lengths[rank - 2] = 2; + lengths[rank - 1] = columns; + strides[rank - 2] = columns + 3; + strides[rank - 1] = 1; + int[] data = Enumerable.Repeat(1, 2 * (columns + 3)).ToArray(); + data[0] = int.MinValue; + int[] output = Enumerable.Repeat(-99, data.Length).ToArray(); + Tensor source = Tensor.Create(data, lengths, strides); + Tensor zero = Tensor.Create(new int[data.Length], lengths, strides); + Tensor destination = Tensor.Create(output, lengths, strides); + + Assert.Throws(() => Tensor.Abs(source.AsReadOnlyTensorSpan(), destination.AsTensorSpan())); + Assert.Throws(() => Tensor.Divide(source.AsReadOnlyTensorSpan(), zero.AsReadOnlyTensorSpan(), destination.AsTensorSpan())); + Assert.Throws(() => Tensor.Divide(source.AsReadOnlyTensorSpan(), 0, destination.AsTensorSpan())); + Assert.Throws(() => Tensor.Divide(1, zero.AsReadOnlyTensorSpan(), destination.AsTensorSpan())); + Assert.All(output, value => Assert.Equal(-99, value)); + } + + [Theory] + [InlineData(3)] + [InlineData(6)] + public static void TensorComparisonBroadcastShapeOverflow(int rank) + { + nint[] xLengths = Enumerable.Repeat((nint)1, rank).ToArray(); + nint[] yLengths = (nint[])xLengths.Clone(); + xLengths[0] = nint.MaxValue; + yLengths[1] = 2; + Tensor x = Tensor.Create([1], xLengths, new nint[rank]); + Tensor y = Tensor.Create([1], yLengths, new nint[rank]); + + Assert.Throws(() => Tensor.EqualsAny(x.AsReadOnlyTensorSpan(), y.AsReadOnlyTensorSpan())); + Assert.Throws(() => Tensor.EqualsAll(x.AsReadOnlyTensorSpan(), y.AsReadOnlyTensorSpan())); + } + [Fact] public static void TensorMultiplyTests() { @@ -3769,6 +3866,44 @@ public static void TensorReshapeHandlesZeroDimensions() Assert.Equal([1, 2, 3, 4], tensor.Reshape([1, 4]).Reshape([4]).ToArray()); } + [Theory] + [InlineData(3, false)] + [InlineData(3, true)] + [InlineData(6, false)] + [InlineData(6, true)] + public static void TensorEmptyReshapeIgnoresProductOverflow(int rank, bool zeroBeforeOverflow) + { + nint[] lengths = new nint[rank]; + Array.Fill(lengths, (nint)1); + lengths[0] = nint.MaxValue; + lengths[1] = zeroBeforeOverflow ? 0 : 2; + lengths[^1] = zeroBeforeOverflow ? 2 : 0; + Tensor empty = Tensor.CreateFromShape([0]); + Tensor expected = Tensor.CreateFromShape(lengths); + + Tensor reshaped = empty.Reshape(lengths); + TensorSpan span = empty.AsTensorSpan().Reshape(lengths); + ReadOnlyTensorSpan readOnlySpan = empty.AsReadOnlyTensorSpan().Reshape(lengths); + Assert.Equal(lengths, reshaped.Lengths); + Assert.Equal(lengths, span.Lengths); + Assert.Equal(lengths, readOnlySpan.Lengths); + Assert.Equal(expected.Strides, reshaped.Strides); + Assert.Equal(expected.Strides, span.Strides); + Assert.Equal(expected.Strides, readOnlySpan.Strides); + Assert.Equal(0, reshaped.FlattenedLength); + Assert.Equal(0, span.FlattenedLength); + Assert.Equal(0, readOnlySpan.FlattenedLength); + + lengths[0] = -1; + Assert.Throws(() => empty.Reshape(lengths)); + Assert.Throws(() => empty.AsTensorSpan().Reshape(lengths)); + Assert.Throws(() => empty.AsReadOnlyTensorSpan().Reshape(lengths)); + lengths[0] = -2; + Assert.Throws(() => empty.Reshape(lengths)); + Assert.Throws(() => empty.AsTensorSpan().Reshape(lengths)); + Assert.Throws(() => empty.AsReadOnlyTensorSpan().Reshape(lengths)); + } + [Theory] [InlineData(4, false)] [InlineData(4, true)] From c2b12efba825b248541682e5cdde6449eaf63aa9 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Wed, 30 Sep 2026 00:14:44 -0700 Subject: [PATCH 5/6] Fix Tensor empty dense stride overflow Use zero automatic strides for empty shapes so unreachable products cannot overflow before the zero-length dimension. Cover every zero position in inline and heap-backed shapes across construction and reshape. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../System.Numerics.Tensors/README.md | 5 +++ .../Numerics/Tensors/netcore/TensorShape.cs | 4 ++ .../tests/TensorTests.cs | 41 ++++++++++++++----- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Numerics.Tensors/README.md b/src/libraries/System.Numerics.Tensors/README.md index ddcf6ca4fd9ee8..f99d892b90fa5a 100644 --- a/src/libraries/System.Numerics.Tensors/README.md +++ b/src/libraries/System.Numerics.Tensors/README.md @@ -21,6 +21,11 @@ Some shape and storage behavior intentionally differs from NumPy: same storage and could not hold distinct values. Zero strides in singleton dimensions and empty destinations do not have this conflict. +When strides are omitted, a shape containing a zero-length dimension has zero strides +in every dimension. Its element count and storage requirement are zero regardless of +the other dimension lengths. Negative lengths remain invalid, and explicitly supplied +strides must still satisfy the normal layout validation. + Overlapping sources and destinations are supported for equal-length dense copies, which use the same overlap-safe behavior as `Span.CopyTo`, and for elementwise operations on identical non-broadcast views. An in-place reversal of a dense tensor also needs no temporary diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorShape.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorShape.cs index 1a46c7d75644e4..3c987bbf720ee1 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorShape.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorShape.cs @@ -102,6 +102,10 @@ private TensorShape(nint linearLength, scoped ReadOnlySpan lengths, scoped if (strides.Length == 0) { + // Empty shapes have no reachable offsets, so zero strides avoid + // overflowing products before the zero-length dimension is visited. + flattenedLength = lengths.Contains(0) ? 0 : 1; + // When no strides are specified, we need to computing them simply // by calculating the product of the lengths at each iteration. diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs index da21a2dcd981f6..361b07db22896e 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs @@ -3867,23 +3867,39 @@ public static void TensorReshapeHandlesZeroDimensions() } [Theory] - [InlineData(3, false)] - [InlineData(3, true)] - [InlineData(6, false)] - [InlineData(6, true)] - public static void TensorEmptyReshapeIgnoresProductOverflow(int rank, bool zeroBeforeOverflow) + [InlineData(3, 0)] + [InlineData(3, 1)] + [InlineData(3, 2)] + [InlineData(6, 0)] + [InlineData(6, 1)] + [InlineData(6, 2)] + [InlineData(6, 3)] + [InlineData(6, 4)] + [InlineData(6, 5)] + public static void TensorEmptyReshapeIgnoresProductOverflow(int rank, int zeroDimension) { nint[] lengths = new nint[rank]; Array.Fill(lengths, (nint)1); - lengths[0] = nint.MaxValue; - lengths[1] = zeroBeforeOverflow ? 0 : 2; - lengths[^1] = zeroBeforeOverflow ? 2 : 0; + int largeDimension = zeroDimension == 0 ? 1 : 0; + int otherDimension = zeroDimension <= 1 ? 2 : 1; + lengths[largeDimension] = nint.MaxValue; + lengths[otherDimension] = 2; + lengths[zeroDimension] = 0; Tensor empty = Tensor.CreateFromShape([0]); Tensor expected = Tensor.CreateFromShape(lengths); + Tensor uninitialized = Tensor.CreateFromShapeUninitialized(lengths); + Tensor fromArray = Tensor.Create(Array.Empty(), lengths); + TensorSpan constructedSpan = new TensorSpan(Array.Empty(), lengths); + ReadOnlyTensorSpan constructedReadOnlySpan = new ReadOnlyTensorSpan(Array.Empty(), lengths); Tensor reshaped = empty.Reshape(lengths); TensorSpan span = empty.AsTensorSpan().Reshape(lengths); ReadOnlyTensorSpan readOnlySpan = empty.AsReadOnlyTensorSpan().Reshape(lengths); + Assert.Equal(new nint[rank], expected.Strides); + Assert.Equal(expected.Strides, uninitialized.Strides); + Assert.Equal(expected.Strides, fromArray.Strides); + Assert.Equal(expected.Strides, constructedSpan.Strides); + Assert.Equal(expected.Strides, constructedReadOnlySpan.Strides); Assert.Equal(lengths, reshaped.Lengths); Assert.Equal(lengths, span.Lengths); Assert.Equal(lengths, readOnlySpan.Lengths); @@ -3894,14 +3910,19 @@ public static void TensorEmptyReshapeIgnoresProductOverflow(int rank, bool zeroB Assert.Equal(0, span.FlattenedLength); Assert.Equal(0, readOnlySpan.FlattenedLength); - lengths[0] = -1; + lengths[largeDimension] = -1; Assert.Throws(() => empty.Reshape(lengths)); Assert.Throws(() => empty.AsTensorSpan().Reshape(lengths)); Assert.Throws(() => empty.AsReadOnlyTensorSpan().Reshape(lengths)); - lengths[0] = -2; + lengths[largeDimension] = -2; Assert.Throws(() => empty.Reshape(lengths)); Assert.Throws(() => empty.AsTensorSpan().Reshape(lengths)); Assert.Throws(() => empty.AsReadOnlyTensorSpan().Reshape(lengths)); + Assert.Throws(() => Tensor.CreateFromShape(lengths)); + Assert.Throws(() => Tensor.CreateFromShapeUninitialized(lengths)); + Assert.Throws(() => Tensor.Create(Array.Empty(), lengths)); + Assert.Throws(() => new TensorSpan(Array.Empty(), lengths)); + Assert.Throws(() => new ReadOnlyTensorSpan(Array.Empty(), lengths)); } [Theory] From c431dcd84450420a08e07edf94c3be3ecf5988a6 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Wed, 30 Sep 2026 06:33:40 -0700 Subject: [PATCH 6/6] Preserve vectorized tangent accuracy across runtimes Use guaranteed fused range reduction on FMA hardware. On other hardware, retain vector evaluation when the reduced remainder bounds cancellation error, and fall back to scalar tangent for affected vectors. Cover floating-point types, reduction boundaries, layouts, special values, and in-place operations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tensors/netcore/TensorPrimitives.Tan.cs | 128 +++++++++++++++--- .../tests/TensorPrimitives.Generic.cs | 78 +++++++++++ .../tests/TensorTests.cs | 108 +++++++++++++++ 3 files changed, 296 insertions(+), 18 deletions(-) diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Tan.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Tan.cs index 8e5c70e8004f93..0637c80aa0b9d3 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Tan.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Tan.cs @@ -3,6 +3,8 @@ using System.Diagnostics; using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; namespace System.Numerics.Tensors { @@ -36,6 +38,14 @@ public static void Tan(ReadOnlySpan x, Span destination) InvokeSpanIntoSpan>(x, destination); } + // A non-fused product dn * pi/2 has absolute rounding error at most u * dn * pi/2, + // where u is the unit roundoff. Requiring |f| >= dn/256 bounds the relative + // reduction error to roughly 410u, including the tail corrections. On + // [-pi/4, pi/4], tan and cot amplify this by less than 2, keeping it below + // 5e-5 for float and 1e-13 for double. dn == 0 needs no range reduction. + // Beyond this bound, scalar tangent is cheaper than software FMA. + private const float TanNonFusedReductionThreshold = 1.0f / 256; + /// T.Tan(x) private readonly struct TanOperator : IUnaryOperator where T : ITrigonometricFunctions @@ -60,6 +70,10 @@ public static void Tan(ReadOnlySpan x, Span destination) // when N is even, = -cot(F) = -1/tan(F) // when N is odd, tan(F) is approximated using a polynomial // obtained from Remez approximation from Sollya. + // + // Range reduction can require fused multiply-add: near an odd multiple of π/2, + // separately rounding N * π/2 loses significant bits of F, which is then + // used as the denominator of -1/tan(F). public static bool Vectorizable => typeof(T) == typeof(float) || typeof(T) == typeof(double); @@ -138,9 +152,22 @@ public static Vector128 Invoke(Vector128 x) dn -= Vector128.Create(AlmHuge); Vector128 f = uxMasked; - f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(-float.Pi / 2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(Pi_Tail2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(Pi_Tail3), f); + if (Fma.IsSupported || AdvSimd.Arm64.IsSupported) + { + f = FusedMultiplyAddOperator.Invoke(dn, Vector128.Create(-float.Pi / 2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector128.Create(Pi_Tail2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector128.Create(Pi_Tail3), f); + } + else + { + f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(-float.Pi / 2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(Pi_Tail2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(Pi_Tail3), f); + if (Vector128.LessThanAny(Vector128.Abs(f), dn * Vector128.Create(TanNonFusedReductionThreshold))) + { + return ApplyScalar(x); + } + } // POLY_EVAL_ODD_15 Vector128 f2 = f * f; @@ -173,9 +200,22 @@ public static Vector256 Invoke(Vector256 x) dn -= Vector256.Create(AlmHuge); Vector256 f = uxMasked; - f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(-float.Pi / 2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(Pi_Tail2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(Pi_Tail3), f); + if (Fma.IsSupported || AdvSimd.Arm64.IsSupported) + { + f = FusedMultiplyAddOperator.Invoke(dn, Vector256.Create(-float.Pi / 2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector256.Create(Pi_Tail2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector256.Create(Pi_Tail3), f); + } + else + { + f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(-float.Pi / 2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(Pi_Tail2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(Pi_Tail3), f); + if (Vector256.LessThanAny(Vector256.Abs(f), dn * Vector256.Create(TanNonFusedReductionThreshold))) + { + return ApplyScalar(x); + } + } // POLY_EVAL_ODD_15 Vector256 f2 = f * f; @@ -208,9 +248,22 @@ public static Vector512 Invoke(Vector512 x) dn -= Vector512.Create(AlmHuge); Vector512 f = uxMasked; - f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(-float.Pi / 2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(Pi_Tail2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(Pi_Tail3), f); + if (Fma.IsSupported || AdvSimd.Arm64.IsSupported) + { + f = FusedMultiplyAddOperator.Invoke(dn, Vector512.Create(-float.Pi / 2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector512.Create(Pi_Tail2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector512.Create(Pi_Tail3), f); + } + else + { + f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(-float.Pi / 2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(Pi_Tail2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(Pi_Tail3), f); + if (Vector512.LessThanAny(Vector512.Abs(f), dn * Vector512.Create(TanNonFusedReductionThreshold))) + { + return ApplyScalar(x); + } + } // POLY_EVAL_ODD_15 Vector512 f2 = f * f; @@ -273,9 +326,22 @@ public static Vector128 Invoke(Vector128 x) // f = |x| - (dn * π/2) Vector128 f = uxMasked; - f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(-double.Pi / 2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(-HalfPi2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(-HalfPi3), f); + if (Fma.IsSupported || AdvSimd.Arm64.IsSupported) + { + f = FusedMultiplyAddOperator.Invoke(dn, Vector128.Create(-double.Pi / 2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector128.Create(-HalfPi2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector128.Create(-HalfPi3), f); + } + else + { + f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(-double.Pi / 2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(-HalfPi2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector128.Create(-HalfPi3), f); + if (Vector128.LessThanAny(Vector128.Abs(f), dn * Vector128.Create((double)TanNonFusedReductionThreshold))) + { + return ApplyScalar(x); + } + } // POLY_EVAL_ODD_29 Vector128 g = f * f; @@ -320,9 +386,22 @@ public static Vector256 Invoke(Vector256 x) // f = |x| - (dn * π/2) Vector256 f = uxMasked; - f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(-double.Pi / 2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(-HalfPi2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(-HalfPi3), f); + if (Fma.IsSupported || AdvSimd.Arm64.IsSupported) + { + f = FusedMultiplyAddOperator.Invoke(dn, Vector256.Create(-double.Pi / 2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector256.Create(-HalfPi2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector256.Create(-HalfPi3), f); + } + else + { + f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(-double.Pi / 2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(-HalfPi2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector256.Create(-HalfPi3), f); + if (Vector256.LessThanAny(Vector256.Abs(f), dn * Vector256.Create((double)TanNonFusedReductionThreshold))) + { + return ApplyScalar(x); + } + } // POLY_EVAL_ODD_29 Vector256 g = f * f; @@ -367,9 +446,22 @@ public static Vector512 Invoke(Vector512 x) // f = |x| - (dn * π/2) Vector512 f = uxMasked; - f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(-double.Pi / 2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(-HalfPi2), f); - f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(-HalfPi3), f); + if (Fma.IsSupported || AdvSimd.Arm64.IsSupported) + { + f = FusedMultiplyAddOperator.Invoke(dn, Vector512.Create(-double.Pi / 2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector512.Create(-HalfPi2), f); + f = FusedMultiplyAddOperator.Invoke(dn, Vector512.Create(-HalfPi3), f); + } + else + { + f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(-double.Pi / 2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(-HalfPi2), f); + f = MultiplyAddEstimateOperator.Invoke(dn, Vector512.Create(-HalfPi3), f); + if (Vector512.LessThanAny(Vector512.Abs(f), dn * Vector512.Create((double)TanNonFusedReductionThreshold))) + { + return ApplyScalar(x); + } + } // POLY_EVAL_ODD_29 Vector512 g = f * f; diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs index 295955d9497ea6..8c2e4a46cf145c 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs @@ -445,6 +445,84 @@ protected override void SetSpecialValues(Span x, Span y) } #region Span -> Destination + public static IEnumerable Tan_RangeReductionInputs() + { + foreach (int length in new[] { 0, 1, 2, 3, 4, 7, 8, 9, 15, 16, 17, 31, 32, 33, 65 }) + { + for (int inputKind = 0; inputKind < 6; inputKind++) + { + yield return new object[] { length, inputKind }; + } + } + } + + [Theory] + [MemberData(nameof(Tan_RangeReductionInputs))] + public void Tan_RangeReduction(int length, int inputKind) + { + using BoundedMemory source = CreateTensor(length); + using BoundedMemory destination = CreateTensor(length); + T[] expected = new T[length]; + T? tolerance = Helpers.DetermineTolerance(doubleTolerance: 3e-13, floatTolerance: 1e-4f); + for (int i = 0; i < length; i++) + { + T pole = (T.CreateChecked(i % 16) + T.CreateChecked(0.5)) * T.Pi; + T value = (i % 4) switch + { + 0 => T.BitDecrement(pole), + 1 => T.BitIncrement(pole), + 2 => -T.BitDecrement(pole), + _ => -T.BitIncrement(pole), + }; + if (i == 0) + { + value = T.CreateChecked(-32.986717f); + } + switch (inputKind) + { + case 1: + value = T.CreateChecked(i % 33 - 16) * T.CreateChecked(0.3125); + break; + case 2: + case 5: + T count = T.CreateChecked(i / 16 * 31 + 21); + T boundary = count * (T.Pi / T.CreateChecked(2)) + + count * T.CreateChecked((inputKind == 2 ? 0.99 : 1.01) / 256); + value = i / 16 % 2 == 0 ? boundary : -boundary; + break; + case 3: + T limit = T.CreateSaturating(typeof(T) == typeof(double) ? 8388608 : 1048576); + value = (i % 4) switch + { + 0 => T.BitDecrement(limit), + 1 => T.BitIncrement(limit), + 2 => -T.BitDecrement(limit), + _ => -T.BitIncrement(limit), + }; + break; + case 4: + value = i == length - 1 ? T.CreateChecked(-32.986717f) : + T.CreateChecked(i % 7 - 3) * T.CreateChecked(0.125); + break; + } + + source[i] = value; + expected[i] = T.Tan(value); + } + + TensorPrimitives.Tan(source.Span, destination.Span); + for (int i = 0; i < length; i++) + { + AssertEqualTolerance(expected[i], destination[i], tolerance); + } + + TensorPrimitives.Tan(source.Span, source.Span); + for (int i = 0; i < length; i++) + { + AssertEqualTolerance(expected[i], source[i], tolerance); + } + } + public static IEnumerable SpanDestinationFunctionsToTest() { // The current trigonometric algorithm depends on hardware FMA support for best precision. diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs index 361b07db22896e..c67294f6678d62 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorTests.cs @@ -121,6 +121,114 @@ public void TensorExtensionsSpanInSpanOut(PerformCalculationSpanInSpanOut }); } + [Theory] + [InlineData(0, 0)] + [InlineData(0, 1)] + [InlineData(0, 3)] + [InlineData(0, 9)] + [InlineData(0, 17)] + [InlineData(0, 33)] + [InlineData(1, 0)] + [InlineData(1, 1)] + [InlineData(1, 3)] + [InlineData(1, 9)] + [InlineData(1, 17)] + [InlineData(1, 33)] + [InlineData(2, 0)] + [InlineData(2, 1)] + [InlineData(2, 3)] + [InlineData(2, 9)] + [InlineData(2, 17)] + [InlineData(2, 33)] + [InlineData(3, 0)] + [InlineData(3, 1)] + [InlineData(3, 3)] + [InlineData(3, 9)] + [InlineData(3, 17)] + [InlineData(3, 33)] + public static void TensorTanPreservesScalarAccuracy(int layout, int columns) + { + Test(layout, columns); + Test(layout, columns); + Test(layout, columns); + + static void Test(int layout, int columns) + where T : unmanaged, IFloatingPointIeee754 + { + nint[] lengths = [2, columns]; + nint[] strides = layout switch + { + 1 => [columns + 3, columns > 1 ? 1 : 0], + 2 => [1, columns > 1 ? 2 : 0], + 3 => [0, columns > 1 ? 1 : 0], + _ => [], + }; + T[] storage = new T[2 * (columns + 3)]; + T[] expected = new T[2 * columns]; + for (int row = 0; row < 2; row++) + { + for (int column = 0; column < columns; column++) + { + T pole = (T.CreateChecked(column % 16) + T.CreateChecked(0.5)) * T.Pi; + T value = (column % 4) switch + { + 0 => T.BitDecrement(pole), + 1 => T.BitIncrement(pole), + 2 => -T.BitDecrement(pole), + _ => -T.BitIncrement(pole), + }; + if (column == 0) + { + value = T.CreateChecked(-32.986717f); + } + else if (column >= 24) + { + value = (column % 5) switch + { + 0 => T.Zero, + 1 => T.NegativeZero, + 2 => T.PositiveInfinity, + 3 => T.NegativeInfinity, + _ => T.NaN, + }; + } + int offset = layout switch + { + 1 => row * (columns + 3) + column, + 2 => row + 2 * column, + 3 => column, + _ => row * columns + column, + }; + storage[offset] = value; + expected[row * columns + column] = T.Tan(value); + } + } + + Tensor source = Tensor.Create(storage, lengths, strides); + AssertResult(Tensor.Tan(source).ToArray()); + + Tensor destination = Tensor.CreateFromShape(lengths); + Tensor.Tan(source.AsReadOnlyTensorSpan(), destination.AsTensorSpan()); + AssertResult(destination.ToArray()); + + if (layout != 3) + { + Tensor.Tan(source.AsReadOnlyTensorSpan(), source.AsTensorSpan()); + AssertResult(source.ToArray()); + } + + void AssertResult(T[] actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < actual.Length; i++) + { + Helpers.AssertEqualWithTolerance(expected[i], actual[i], + Helpers.DetermineTolerance(doubleTolerance: 3e-13, floatTolerance: 1e-4f)); + } + } + } + } + public delegate T PerformSpanInTOut(scoped in ReadOnlyTensorSpan input); public delegate T PerformCalculationSpanInTOut(ReadOnlySpan input); public static IEnumerable SpanInFloatOutData()