-
Couldn't load subscription status.
- Fork 5.2k
Description
Rationale
.NET provides a broad range of support for various development domains, ranging from the creation of performance-oriented framework code to the rapid development of cloud native services, and beyond.
In recent years, especially with the rise of AI and machine learning, there has been a prevalent push towards improving numerical support and allowing developers to more easily write and consume general purpose and reusable algorithms that work with a range of types and scenarios. While .NET's support for scalar algorithms and fixed-sized vectors/matrices is strong and continues to grow, its built-in library support for other concepts, such as tensors and arbitrary length vectors/matrices, could benefit from additional improvements. Today, developers writing .NET applications, services, and libraries currently may need to seek external dependencies in order to utilize functionality that is considered core or built-in to other ecosystems. In particular, for developers incorporating AI and copilots into their existing .NET applications and services, we strive to ensure that the core numerics support necessary to be successful is available and efficient, and that .NET developers are not forced to seek out non-.NET solutions in order for their .NET projects to be successful.
API Proposal
This is extracted from dotnet/designs#316 and the design doc will be updated based on the API review here.
Native Index and Range Types
namespace System.Buffers;
public readonly struct NIndex : IEquatable<NIndex>
{
public NIndex(nint value, bool fromEnd = false);
public NIndex(Index value);
public static NIndex End { get; }
public static NIndex Start { get; }
public bool IsFromEnd { get; }
public nint Value { get; }
public static implicit operator NIndex(nint value);
public static implicit operator NIndex(Index value);
public static explicit operator Index(NIndex value);
public static explicit operator checked Index(NIndex value);
public static NIndex FromEnd(nint value);
public static NIndex FromStart(nint value);
public static Index ToIndex();
public static Index ToIndexUnchecked();
public nint GetOffset(nint length);
// IEquatable<NIndex>
public bool Equals(NIndex other);
}
public readonly struct NRange : IEquatable<NRange>
{
public NRange(NIndex start, NIndex end);
public NRange(Range value);
public static NRange All { get; }
public NIndex End { get; }
public NIndex Start { get; }
public static explicit operator Range(NRange value);
public static explicit operator checked Range(NRange value);
public static NRange EndAt(NIndex end);
public (nint Offset, nint Length) GetOffsetAndLength(nint length);
public static NRange StartAt(NIndex start);
public static Range ToRange();
public static Range ToRangeUnchecked();
// IEquatable<NRange>
public bool Equals(NRange other);
}Multi-dimensional Span Types
namespace System.Numerics.Tensors;
public ref struct TensorSpan<T>
{
public TensorSpan(void* data, nint dataLength, ReadOnlySpan<nint> lengths);
public TensorSpan(void* data, nint dataLength, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public TensorSpan(Span<T> span);
public TensorSpan(Span<T> span, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public TensorSpan(T[]? array);
public TensorSpan(T[]? array, int start, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public TensorSpan(T[]? array, Index startIndex, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
// Should start just be `nint` and represent the linear index?
public TensorSpan(Array? array);
public TensorSpan(Array? array, ReadOnlySpan<int> start, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public TensorSpan(Array? array, ReadOnlySpan<Index> startIndex, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
// Should we have variants for common ranks (consider 2-5):
// public TensorSpan(T[,]? array);
// public TensorSpan(T[,]? array, ReadOnlySpan<int> start, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
// public TensorSpan(T[,]? array, ReadOnlySpan<Index> startIndex, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public static TensorSpan<T> Empty { get; }
public bool IsEmpty { get; }
public nint FlattenedLength { get; }
public int Rank { get; }
public ReadOnlySpan<nint> Lengths { get; }
public ReadOnlySpan<nint> Strides { get; }
public ref T this[params scoped ReadOnlySpan<nint> indexes] { get; }
public ref T this[params scoped ReadOnlySpan<NIndex> indexes] { get; }
public TensorSpan<T> this[params scoped ReadOnlySpan<NRange> ranges] { get; set; }
// These work like Span<T>, comparing the byref, lengths, and strides, not element-wise
public static bool operator ==(TensorSpan<T> left, TensorSpan<T> right);
public static bool operator !=(TensorSpan<T> left, TensorSpan<T> right);
public static explicit operator TensorSpan<T>(Array? array);
public static implicit operator TensorSpan<T>(T[]? array);
// Should we have variants for common ranks (consider 2-5):
// public static implicit operator TensorSpan<T>(T[,]? array);
public static implicit operator ReadOnlyTensorSpan<T>(TensorSpan<T> span);
public void Clear();
public void CopyTo(TensorSpan<T> destination);
public void Fill(T value);
public void FlattenTo(Span<T> destination);
public Enumerator GetEnumerator();
public ref T GetPinnableReference();
public TensorSpan<T> Slice(params ReadOnlySpan<NIndex> indexes);
public TensorSpan<T> Slice(params ReadOnlySpan<NRange> ranges);
public bool TryCopyTo(TensorSpan<T> destination);
public bool TryFlattenTo(Span<T> destination);
[ObsoleteAttribute("Equals() on TensorSpan will always throw an exception. Use the equality operator instead.")]
public override bool Equals(object? obj);
[ObsoleteAttribute("GetHashCode() on TensorSpan will always throw an exception.")]
public override int GetHashCode();
// Do we want `Array ToArray()` to mirror Span<T>?
public ref struct Enumerator
{
public ref readonly T Current { get; }
public bool MoveNext();
}
}
public ref struct ReadOnlyTensorSpan<T>
{
public ReadOnlyTensorSpan(void* data, nint dataLength, ReadOnlySpan<nint> lengths);
public ReadOnlyTensorSpan(void* data, nint dataLength, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public ReadOnlyTensorSpan(T[]? array);
public ReadOnlyTensorSpan(T[]? array, int start, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public ReadOnlyTensorSpan(T[]? array, Index startIndex, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
// Should start just be `nint` and represent the linear index?
public ReadOnlyTensorSpan(Array? array);
public ReadOnlyTensorSpan(Array? array, ReadOnlySpan<int> start, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public ReadOnlyTensorSpan(Array? array, ReadOnlySpan<Index> startIndices, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
// Should we have variants for common ranks (consider 2-5):
// public ReadOnlyTensorSpan(T[,]? array);
// public ReadOnlyTensorSpan(T[,]? array, ReadOnlySpan<int> start, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
// public ReadOnlyTensorSpan(T[,]? array, ReadOnlySpan<Index> startIndices, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public static ReadOnlyTensorSpan<T> Empty { get; }
public bool IsEmpty { get; }
public nint Length { get; }
public int Rank { get; }
public ReadOnlySpan<nint> Lengths { get; }
public ReadOnlySpan<nint> Strides { get; }
public ref readonly T this[params scoped ReadOnlySpan<nint> indexes] { get; }
public ref readonly T this[params scoped ReadOnlySpan<NIndex> indexes] { get; }
public ReadOnlyTensorSpan<T> this[params scoped ReadOnlySpan<NRange> ranges] { get; set; }
// These work like ReadOnlySpan<T>, comparing the byref, lengths, and strides, not element-wise
public static bool operator ==(ReadOnlyTensorSpan<T> left, ReadOnlyTensorSpan<T> right);
public static bool operator !=(ReadOnlyTensorSpan<T> left, ReadOnlyTensorSpan<T> right);
public static explicit operator ReadOnlyTensorSpan<T>(Array? array);
public static implicit operator ReadOnlyTensorSpan<T>(T[]? array);
// Should we have variants for common ranks (consider 2-5):
// public static implicit operator ReadOnlyTensorSpan<T>(T[,]? array);
public void CopyTo(Span<T> destination);
public void FlattenTo(Span<T> destination);
public Enumerator GetEnumerator();
public ref readonly T GetPinnableReference();
public ReadOnlyTensorSpan<T> Slice(params ReadOnlySpan<NIndex> indexes);
public ReadOnlyTensorSpan<T> Slice(params ReadOnlySpan<NRange> ranges);
public bool TryCopyTo(TensorSpan<T> destination);
public bool TryFlattenTo(Span<T> destination);
[ObsoleteAttribute("Equals() on ReadOnlyTensorSpan will always throw an exception. Use the equality operator instead.")]
public override bool Equals(object? obj);
[ObsoleteAttribute("GetHashCode() on ReadOnlyTensorSpan will always throw an exception.")]
public override int GetHashCode();
// Do we want `Array ToArray()` to mirror Span<T>?
public ref struct Enumerator
{
public ref readonly T Current { get; }
public bool MoveNext();
}
}Tensor Type
public interface IReadOnlyTensor<TSelf, T> : IEnumerable<T>
where TSelf : IReadOnlyTensor<TSelf, T>
{
// Should there be APIs for creating from an array, TensorSpan, etc
static abstract TSelf Empty { get; }
bool IsEmpty { get; }
bool IsPinned { get; }
nint Length { get; }
int Rank { get; }
T this[params ReadOnlySpan<nint> indexes] { get; }
T this[params ReadOnlySpan<NIndex> indexes] { get; }
TSelf this[params scoped ReadOnlySpan<NRange> ranges] { get; }
ReadOnlyTensorSpan<T> AsReadOnlyTensorSpan();
ReadOnlyTensorSpan<T> AsReadOnlyTensorSpan(params scoped ReadOnlySpan<nint> start);
ReadOnlyTensorSpan<T> AsReadOnlyTensorSpan(params scoped ReadOnlySpan<NIndex> startIndex);
ReadOnlyTensorSpan<T> AsReadOnlyTensorSpan(params scoped ReadOnlySpan<NRange> ranges);
void CopyTo(TensorSpan<T> destination);
void FlattenTo(TensorSpan<T> destination);
// These are not properties so that structs can implement the interface without allocating:
void GetLengths(Span<nint> destination);
void GetStrides(Span<nint> destination);
ref readonly T GetPinnableReference();
TSelf Slice(params scoped ReadOnlySpan<nint> start);
TSelf Slice(params scoped ReadOnlySpan<NIndex> startIndices);
TSelf Slice(params scoped ReadOnlySpan<NRange> ranges);
bool TryCopyTo(TensorSpan<T> destination);
bool TryFlattenTo(TensorSpan<T> destination);
}
public interface ITensor<TSelf, T> : IReadOnlyTensor<TSelf, T>
where TSelf : ITensor<TSelf, T>
{
static TSelf Create(ReadOnlySpan<nint> lengths, bool pinned = false);
static TSelf Create(ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides, bool pinned = false);
static TSelf CreateUninitialized(ReadOnlySpan<nint> lengths, bool pinned = false);
static TSelf CreateUninitialized(ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides, bool pinned = false);
bool IsReadOnly { get; }
new T this[params ReadOnlySpan<nint> indexes] { get; set; }
new T this[params ReadOnlySpan<NIndex> indexes] { get; set; }
new TSelf this[params scoped ReadOnlySpan<NRange> ranges] { get; set; }
TensorSpan<T> AsTensorSpan();
TensorSpan<T> AsTensorSpan(params scoped ReadOnlySpan<nint> start);
TensorSpan<T> AsTensorSpan(params scoped ReadOnlySpan<NIndex> startIndex);
TensorSpan<T> AsTensorSpan(params scoped ReadOnlySpan<NRange> ranges);
void Clear();
void Fill(T value);
new ref T GetPinnableReference();
}
public sealed class Tensor<T> : ITensor<Tensor<T>, T>
{
static TSelf ITensor<T>.Create(ReadOnlySpan<nint> lengths, bool pinned = false);
static TSelf ITensor<T>.Create(ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides, bool pinned = false);
static TSelf ITensor<T>.CreateUninitialized(ReadOnlySpan<nint> lengths, bool pinned = false);
static TSelf ITensor<T>.CreateUninitialized(ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides, bool pinned = false);
public static Tensor<T> Empty { get; }
public bool IsEmpty { get; }
public bool IsPinned { get; }
public nint Length { get; }
public int Rank { get; }
public ReadOnlySpan<nint> Lengths { get; }
public ReadOnlySpan<nint> Strides { get; }
bool ITensor<T>.IsReadOnly { get; }
public ref T this[params ReadOnlySpan<nint> indexes] { get; }
public ref T this[params ReadOnlySpan<NIndex> indexes] { get; }
public Tensor<T> this[params ReadOnlySpan<NRange> ranges] { get; set; }
T IReadOnlyTensor<T>.this[params ReadOnlySpan<nint> indexes] { get; }
T IReadOnlyTensor<T>.this[params ReadOnlySpan<NIndex> indexes] { get; }
Tensor<T> IReadOnlyTensor<T>.this[params ReadOnlySpan<NRange> ranges] { get; set; }
public static implicit operator TensorSpan<T>(Tensor<T> value);
public static implicit operator ReadOnlyTensorSpan<T>(Tensor<T> value);
public TensorSpan<T> AsTensorSpan();
public TensorSpan<T> AsTensorSpan(params ReadOnlySpan<nint> start);
public TensorSpan<T> AsTensorSpan(params ReadOnlySpan<NIndex> startIndex);
public TensorSpan<T> AsTensorSpan(params ReadOnlySpan<NRange> ranges);
public ReadOnlyTensorSpan<T> AsReadOnlyTensorSpan();
public ReadOnlyTensorSpan<T> AsReadOnlyTensorSpan(params ReadOnlySpan<nint> start);
public ReadOnlyTensorSpan<T> AsReadOnlyTensorSpan(params ReadOnlySpan<NIndex> startIndex);
public ReadOnlyTensorSpan<T> AsReadOnlyTensorSpan(params ReadOnlySpan<NRange> ranges);
public void Clear();
public void CopyTo(TensorSpan<T> destination);
public void FlattenTo(TensorSpan<T> destination);
public void Fill(T value);
public Enumerator GetEnumerator();
public ref T GetPinnableReference();
public Tensor<T> Slice(params ReadOnlySpan<nint> start);
public Tensor<T> Slice(params ReadOnlySpan<NIndex> startIndices);
public Tensor<T> Slice(params ReadOnlySpan<NRange> ranges);
public bool TryCopyTo(TensorSpan<T> destination);
public bool TryFlattenTo(TensorSpan<T> destination);
void IReadOnlyTensor<T>.GetLengths(Span<nint> destination);
void IReadOnlyTensor<T>.GetStrides(Span<nint> destination);
ref readonly T IReadOnlyTensor<T>.GetPinnableReference();
// The behavior of Equals, GetHashCode, and ToString needs to be determined
// For ToString, is the following sufficient to help mitigate potential issues:
// public string ToString(params ReadOnlySpan<nint> maximumLengths);
public ref struct Enumerator
{
public ref readonly T Current { get; }
public bool MoveNext();
}
}
public static partial class Tensor
{
public static TSelf Create(ReadOnlySpan<nint> lengths, bool pinned = false);
public static TSelf Create(ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides, bool pinned = false);
public static TSelf CreateUninitialized(ReadOnlySpan<nint> lengths, bool pinned = false);
public static TSelf CreateUninitialized(ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides, bool pinned = false);
// Effectively mirror the TensorPrimitives surface area. Following the general pattern
// where we will return a new tensor and an overload that takes in the destination explicitly.
// public static Tensor<T> Add<T>(Tensor<T> left, Tensor<T> right)
// where T : IAdditionOperators<T, T, T>;
// public static void Add<T>(TensorSpan<T> left, ReadOnlyTensorSpan<T> right)
// where T : IAdditionOperators<T, T, T>;
// public static TTensor Add<TTensor, T>(TensorSpan<T> left, ReadOnlyTensorSpan<T> right)
// where T : IAdditionOperators<T, T, T>
// where TTensor : ITensor<TTensor, T>;
// Consider whether we should have one named `*InPlace`, for easier chaining, such as:
// public static Span<T> AddInPlace<T>(TensorSpan<T> left, ReadOnlyTensorSpan<T> right)
// where T : IAdditionOperators<T, T, T>;
// The language ideally has extension operators such that we can define `operator +` instead of `Add`
// Without this support, we end up having to have `TensorNumber<T>`, `TensorBinaryInteger<T>`, etc
// as we otherwise cannot correctly expose the operators based on what `T` supports
// APIs that would return `bool` like `GreaterThan` are split into 3. Following the general
// pattern already established for our SIMD vector types.
// * public static ITensor<T> GreaterThan(ITensor<T> x, ITensor<T> y);
// * public static bool GreaterThanAny<T>(ITensor<T> x, ITensor<T> y);
// * public static bool GreaterThanAll<T>(ITensor<T> x, ITensor<T> y);
}Additional Notes
We know we need to support having the backing memory allocated in native so that large array allocations can work. Our current thinking is that this will be a distinct/separate type (possibly named NativeTensor<T>), but are still finalizing the details on how lifetimes and ownership will work correctly.
By default, operations will allocate and return a Tensor<T> from operations. As part of the above support for NativeTensor<T> we are looking at ways the users can override this default behavior, it will likely come with some ThreadStatic member that allows users to provide a custom allocator.