Repository navigation
Description
Background and motivation
We have exposed lots of functions we can use with Tensor<T>. These currently all operate on the entire backing memory of the Tensor. The problem with this is that many times you want to do the operation on whole dimensions, rather than the whole data, and we have no easy way of providing this currently. For a very simple example say your Tensor<T> is 2d, think of it like an Excel table. In Excel we can sum the whole table, but we can also sum each row, for example. This is the functionality we need to add to Tensor<T>.
The approach we have taken means we don't have to add overloads to every operation that could do this type of behavior. Currently we are planning on these methods needing this behavior.
CosineSimilarity
StdDev
Average
Max
MaxMagnitude
MaxMagnitudeNumber
MaxNumber
Min
MinMagnitude
MinMagnitudeNumber
MinNumber
Norm
Product
SoftMax
Sum
SumOfSquares
IndexOfMax
IndexOfMaxMagnitude
IndexOfMin
IndexOfMinMagnitude
API Proposal
namespace System.Numerics.Tensors;
public interface IReadOnlyTensor<TSelf, T> : IEnumerable<T>
where TSelf : IReadOnlyTensor<TSelf, T>
{
//used to easily get a sub dimension of a tensor. Like all the Rows of a tensor for example.
TSelf SliceAlongDimension(int dimension, nint index);
}
public static partial class Tensor
{
// These are for things like Sum that take in a tensor and return a single T.
public delegate T TensorAggregationPredicate<T>(in ReadOnlyTensorSpan<T> tensor);
public static Tensor<T> AggregateMany<T>(this Tensor<T> tensor, TensorAggregationPredicate<T> func, int dimension = -1);
// These are for things like CosineSimilarity that take in a tensor and return a new tensor as well.
public delegate Tensor<T> TensorSelectionPredicate<T>(in ReadOnlyTensorSpan<T> tensor);
public static Tensor<T> SelectMany<T>(this Tensor<T> tensor, TensorSelectionPredicate<T> func, int dimension = -1);
public static Tensor<T> ToTensor<T>(this IEnumerable<Tensor<T>> source);
}
public sealed class Tensor<T>
: ITensor<Tensor<T>, T>
{
public DimensionCollection Dimensions {get;}
public sealed class DimensionCollection
: System.Collections.Generic.ICollection<Tensor<T>>,
System.Collections.Generic.IEnumerable<Tensor<T>>,
System.Collections.Generic.IReadOnlyCollection<Tensor<T>>,
System.Collections.ICollection
{
public int Count {get;}
public void CopyTo(Tensor<T>[] array , int index)
public Enumerator GetEnumerator();
//All else explicitly implemented and match what Dictionary does
public struct Enumerator : IEnumerator<Tensor<T>>
{
internal Enumerator(Tensor<T> tensor, int dimension);
public bool MoveNext();
public void Reset();
public void Dispose();
Tensor<T> IEnumerator<Tensor<T>>.Current;
object? IEnumerator.Current;
}
}
}
public readonly ref struct ReadOnlyTensorSpan<T>
{
public DimensionCollection Dimensions {get;}
public ref struct DimensionCollection
: System.Collections.Generic.ICollection<Tensor<T>>,
System.Collections.Generic.IEnumerable<Tensor<T>>,
System.Collections.Generic.IReadOnlyCollection<Tensor<T>>,
System.Collections.ICollection
{
public int Count {get;}
public void CopyTo(Tensor<T>[] array , int index)
public Enumerator GetEnumerator();
//All else explicitly implemented and match what Dictionary does
public ref struct Enumerator : IEnumerator<Tensor<T>>
{
internal Enumerator(Tensor<T> tensor, int dimension);
public bool MoveNext();
public ref readonly ReadOnlyTensorSpan<T> Current;
}
}
}
public ref struct TensorSpan<T>
{
public DimensionCollection Dimensions {get;}
public ref struct DimensionCollection
: System.Collections.Generic.ICollection<Tensor<T>>,
System.Collections.Generic.IEnumerable<Tensor<T>>,
System.Collections.Generic.IReadOnlyCollection<Tensor<T>>,
System.Collections.ICollection
{
public int Count {get;}
public void CopyTo(Tensor<T>[] array , int index)
public Enumerator GetEnumerator();
//All else explicitly implemented and match what Dictionary does
public ref struct Enumerator : IEnumerator<Tensor<T>>
{
internal Enumerator(Tensor<T> tensor, int dimension);
public bool MoveNext();
public ref TensorSpan<T> Current;
}
}
}API Usage
Tensor<int> t1 = Tensor.Create([1,2,3,4,5,6,7,8,9], [3,3]);
// Will be [1,2,3]
Tensor<int> slice = t1.SliceAlongDimension(0,0);
// Will be [7, 8, 9]
slice = t1.SliceAlongDimension(0,2);
for (int i = 0; i < 3; i++)
{
// Get each slice
t1.SliceAlongDimension(0,i);
// Can now do something which each sub slice.
}
Tensor<int> tensor = Tensor.Create([1, 2, 3, 4], [2, 2]);
// Sum tensor will be [3, 7]
Tensor<int> sum = t.AggregateMany(t => Sum(t), 0);
// CosineTensor would be [1,1,1]
Tensor<float> tensor1 = Create<float>([1, 2, 3, 4, 5, 6, 7, 8, 9], [3, 3], [], false);
Tensor<float> cosineTensor = tensor1.SelectMany(t => CosineSimilarity<T>(t, t), 0);Alternative Designs
We could explicitly overload every method that has this behavior. That would mean an additional 20 overloads just for now. As time goes on this number would grow. If a user wanted to do this with a method we hadn't overloaded yet, they would either have to do it themselves (which we have had happen recently), or they would have to wait. Our current approach allows us to avoid both of these problems.
Risks
Low risk because it's a preview type and its all new features.
Activity
tjwald commentedon Mar 3, 2025
Looks great! Can you give examples from higher order tensors? For example, how would I softmax a 3d tensor across the 0th dimension such that the softmax works on 2d slices?
tjwald commentedon Mar 3, 2025
Just noticed:
The default
dimension = -1- what is the behavior?And:
What is the behavior for this example:
dotnet-policy-service commentedon Mar 3, 2025
Tagging subscribers to this area: @dotnet/area-system-numerics-tensors
See info in area-owners.md if you want to be subscribed.
bartonjs commentedon Mar 4, 2025
Video
tjwald commentedon Mar 5, 2025
Regarding:
After the API design review, I don't think that this is intuitive as I thought in the beginning.
In multidimensional cases, the index parameter isn't intuitive to me - the index would run higher than any given dimension in the tensor, and calculating it isn't trivial.
Given a 3-dimensional tensor, to enumerate with for loops on 2 dims and slice on the third would look like so:
Which isn't an intuitive way to loop over an "n dimensional array".
So more likely usage would look like so:
From this usage it is unclear (as also discussed in the api review) what is the shape and content of the slice in the loop.
I would propose to change to instead pass a
params ReadOnlySpan<nint> indices, with this API:And then:
And then from my previous question:
Would be:
But this is just an indexer:
But this doesn't work since:
Returns a scalar.
Can the index be changed to return a Tensor?
To enable getting a scalar back, I would add the following API instead:
Regarding:
How will the dimension get passed to this Enumerator?
During the api review it was discussed that the usage should be:
But how can I set the dimension? And given my previous change to Slice - we would need to change it to
ReadOnlySpan<nint>tjwald commentedon Mar 18, 2025
@michaelgsharp @tannergooding making sure you saw my comment on the design review
tannergooding commentedon Mar 24, 2025
dimensionis[0, Rank).indexis then[0, Product(Lengths[..(dimension + 1)])]That is, consider a
2x3x4tensor. This has a depth of2, each of which has3rows, each of which has4columns. The entire tensor then has6total rows (3rows to a depth of2) and24total elements (4columns across6total rows).Thus the ranges are as follows, which allows you to iterate all 0-dimensional (elements), all 1-dimensional (rows), or all 2-dimensional tensors contained in the 3-dimensional tensor:
You'd normally just get the
CountviaDimensions[dimension].Countwhich tells you how many total entries exist for that dimension (2,6,24, etc)This makes indexing much more expensive and is already possible using
Sliceitself or the indexer that takes a range. The purpose ofSliceAlongDimensionis to make it cheaper and more efficient to iterate a particular dimensional slice and therefore to skip the normal slicing considerations that might exist.No. This would defeat the typical expectation of an indexer. There is a separate indexer that takes a range and where you can do
tensor[0.., .., ..]to get the "equivalent" ofSliceAlongDimension(0, 0).Not quite. It should rather be the following, which specifies you want to iterate all rows in the
2x3x4:That is, it is equivalent to:
tjwald commentedon Mar 24, 2025
Makes sense @tannergooding .
I don't see an indexer defined on DimensionCollection type. Is that a mistake?