-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tensor<T> and supporting types #100924
Comments
namespace System;
public readonly struct NativeIndex : IEquatable<NativeIndex>
{
public NativeIndex(nint value, bool fromEnd = false);
public NativeIndex(Index value);
public static NativeIndex End { get; }
public static NativeIndex Start { get; }
public bool IsFromEnd { get; }
public nint Value { get; }
public static implicit operator NativeIndex(nint value);
public static implicit operator NativeIndex(Index value);
public static explicit operator Index(NativeIndex value);
public static explicit operator checked Index(NativeIndex value);
public Index ToIndex();
public static NativeIndex FromEnd(nint value);
public static NativeIndex FromStart(nint value);
public nint GetOffset(nint length);
}
public readonly struct NativeRange : IEquatable<NativeRange>
{
public NativeRange(NativeIndex start, NativeIndex end);
public NativeRange(Range range);
public static NativeRange All { get; }
public NativeIndex End { get; }
public NativeIndex Start { get; }
public static explicit operator Range(NativeRange value);
public static explicit operator checked Range(NativeRange value);
public Range ToRange();
public static NativeRange EndAt(NativeIndex end);
public (nint Offset, nint Length) GetOffsetAndLength(nint length);
public static NativeRange StartAt(NativeIndex start);
} namespace System;
public readonly ref struct SpanND<T>
{
public SpanND(void* pointer, ReadOnlySpan<nint> lengths);
public SpanND(void* pointer, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public SpanND(Span<T> span);
public SpanND(Span<T> span, ReadOnlySpan<nint> offsets, ReadOnlySpan<nint> lengths);
public SpanND(Array? array);
public SpanND(Array? array, ReadOnlySpan<nint> offsets, ReadOnlySpan<nint> lengths);
// Do we want overloads for common dimensions like `T[,]` and `T[,,]`?
public static SpanND<T> Empty { get; }
public bool IsEmpty { get; }
public ReadOnlySpan<nint> Lengths { get; }
public nint LinearLength { get; }
public int Rank { get; }
public ReadOnlySpan<nint> Strides { get; }
public ref T this[params ReadOnlySpan<nint> indices] { get; }
public ref T this[params ReadOnlySpan<NativeIndex> indices] { get; }
public static bool operator ==(SpanND<T> left, SpanND<T> right);
public static bool operator !=(SpanND<T> left, SpanND<T> right);
public static implicit operator SpanND<T>(Array array);
public static implicit operator SpanND<T>(Span<T> span);
public static implicit operator ReadOnlySpanND<T>(SpanND<T> span);
public void Clear();
public void CopyTo(SpanND<T> destination);
public bool TryCopyTo(SpanND<T> destination);
public void FlattenTo(Span<T> destination);
public bool TryFlattenTo(Span<T> destination);
public void Fill(T value);
public Enumerator GetEnumerator();
public ref T GetPinnableReference();
public SpanND<T> Slice(params ReadOnlySpan<NativeIndex> indices);
public SpanND<T> Slice(params ReadOnlySpan<NativeRange> ranges);
// Do we want `Array ToArray()` to mirror Span<T>?
public ref struct Enumerator
{
public ref readonly T Current { get; }
public bool MoveNext();
}
}
public readonly ref struct ReadOnlySpanND<T>
{
public ReadOnlySpanND(void* pointer, ReadOnlySpan<nint> lengths);
public ReadOnlySpanND(void* pointer, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public ReadOnlySpanND(ReadOnlySpan<T> span);
public ReadOnlySpanND(ReadOnlySpan<T> span, ReadOnlySpan<nint> starts, ReadOnlySpan<nint> lengths);
public ReadOnlySpanND(Array? array);
public ReadOnlySpanND(Array? array, ReadOnlySpan<nint> starts, ReadOnlySpan<nint> lengths);
// Do we want overloads for common dimensions like `T[,]` and `T[,,]`?
public static ReadOnlySpanND<T> Empty { get; }
public bool IsEmpty { get; }
public ReadOnlySpan<nint> Lengths { get; }
public nint LinearLength { get; }
public int Rank { get; }
public ReadOnlySpan<nint> Strides { get; }
public ref readonly T this[params ReadOnlySpan<nint> indices] { get; }
public ref readonly T this[params ReadOnlySpan<NativeIndex> indices] { get; }
public static bool operator ==(ReadOnlySpanND<T> left, ReadOnlySpanND<T> right);
public static bool operator !=(ReadOnlySpanND<T> left, ReadOnlySpanND<T> right);
public static implicit operator ReadOnlySpanND<T>(Array array);
public void CopyTo(SpanND<T> destination);
public bool TryCopyTo(SpanND<T> destination);
public void FlattenTo(Span<T> destination);
public bool TryFlattenTo(Span<T> destination);
public Enumerator GetEnumerator();
public ref readonly T GetPinnableReference();
public ReadOnlySpanND<T> Slice(params ReadOnlySpan<NativeIndex> indices);
public ReadOnlySpanND<T> Slice(params ReadOnlySpan<NativeRange> ranges);
// Do we want `Array ToArray()` to mirror Span<T>?
public ref struct Enumerator
{
public ref readonly T Current { get; }
public bool MoveNext();
}
} |
... Isn't the ability to return buffers backed by a native buffer part of the point of |
I really, really don't think we should expose |
If we're going with abbreviations in the name (i.e. not something like SpanMultiDimensional), I would definitely go with SpanMD over SpanND. I can immediately understand what the MD in SpanMD means, but I would have no idea what a SpanND is - I would have to look at the documentation to understand that it's a multi-dimensional span... and even after I look at the documentation and learn that, I would still have no idea what the ND stands for (and I would probably think the N stands for native, so native something), whereas SpanMD is immediately clear. |
As far as the namespace goes, System.Numerics might be better than System (for SpanMD, not for NativeIndex & NativeRange), since I imagine this will be a lot less common than regular Span, and only used in specialized domains related to numerics. |
public SpanND(Span<T> span, ReadOnlySpan<nint> offsets, ReadOnlySpan<nint> lengths); How would this constructor work? It mirrors the |
This is not going to happen. It has been suggested and discussed in depth for years and it is "too breaking". There is simply too much existing code that assumes the lengths can only be |
A lot of the other feedback given was already covered in API review and the proposal will be updated accordingly before it goes back to review. Most of the questions of "why" can be answered by prior precedent, convenience, or it being historical artifacts based on the point in time the prior precedent was implemented. .NET then tries to stay overall consistent unless there is sufficient reason to deviate. For example, we use Cases where we have opted to explicitly deviate include places that don't have more widespread prior art, like |
Somewhat unrelated but is there a reason for |
What about HyperSpan since it's a view into a hypercube, basically... NativeIndex / NativeRange... I feel that "Native" implies to me that it's doing something with unmanaged memory (as NativeTensor is envisioned to do). As for FlattenTo... What is this method envisioned to be used for? If it's for interop or persistence, it would probably need a parameter to specify the target layout (row/column major). |
Also covered in the review. The general premise is that the natural name for a type of known size would be
Native has many different meanings in many different contexts, but historically in .NET it has meant platform-sized. For example,
It is a common API for libraries providing such functionality and is simply meant to get a linear view of the data via a copy operation (as compared to a reshape operation that may make data access effectively random). You cannot trivially specify target layout via parameterization as it is not as simple as "row vs column major", especially as you get to higher dimensionality. As per API review, this would be done via the explicit Tensors themselves will stored "row-major" as that is how .NET is explicitly documented to work and how most other languages/runtimes (and physical memory) actually work. Which is to say given an index |
If there was an implicit conversion from an index to a range, I don't think it should create a range of length one, but instead a range from the specified index to the end, because that's how span's Slice behaves when you only pass an index and not a range. |
To throw a name suggestion out there, one that comes to mind is public SpanND<T> Slice(params ReadOnlySpan<NativeIndex> indices);
public SpanND<T> Slice(params ReadOnlySpan<NativeRange> ranges); Should there be an additional // Do we want `Array ToArray()` to mirror Span<T>? Judging by the return type, I assume this would allocate a multi-dimensional array? Would it make sense to also have |
Why not (Though |
I'm partial to Ultimately, while I agree with @tannergooding that the name should be short, "ND" is too short and unclear IMO. I'd suggest either |
We determined these types should not be restricted to only
It's worth explicitly noting that one of the reasons that This isn't some arbitrary thing that .NET would be opting to do that differs from the rest of the broader ecosystem, it would only differ initially from the more standard .NET convention. However, .NET has often used abbreviations where it does make sense, especially for more core or primitive types (it is I disagree that |
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);
// Should this always do checked or unchecked, how do users get the other?
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);
// Should this always do checked or unchecked, how do users get the other?
public static Range ToRange();
public static Range ToRangeUnchecked();
// IEquatable<NRange>
public bool Equals(NRange other);
} namespace System.Numerics.Tensors;
public ref struct TensorSpan<T>
{
// Should this be in the shape of the following instead:
// public SpanND(void* pointer, nint length, ReadOnlySpan<nint> shape);
// public SpanND(void* pointer, nint length, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
public SpanND(void* pointer, ReadOnlySpan<nint> shape);
public SpanND(void* pointer, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
public SpanND(Span<T> span);
public SpanND(Span<T> span, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
public SpanND(T[]? array);
public SpanND(T[]? array, int start, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
public SpanND(T[]? array, Index startIndex, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
// Should start just be `nint` and represent the linear index?
public SpanND(Array? array);
public SpanND(Array? array, ReadOnlySpan<int> start, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
public SpanND(Array? array, ReadOnlySpan<Index> startIndex, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
// Should we have variants for common ranks (consider 2-5):
// public SpanND(T[,]? array);
// public SpanND(T[,]? array, ReadOnlySpan<int> start, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
// public SpanND(T[,]? array, ReadOnlySpan<Index> startIndex, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
public static SpanND<T> Empty { get; }
public bool IsEmpty { get; }
public nint Length { get; }
public int Rank { get; }
public ReadOnlySpan<nint> Shape { get; }
public ReadOnlySpan<nint> Strides { get; }
public ref T this[params scoped ReadOnlySpan<nint> indices] { get; }
public ref T this[params scoped ReadOnlySpan<NativeIndex> indices] { get; }
public SpanND<T> this[params scoped ReadOnlySpan<NativeRange> ranges] { get; set; }
// These work like Span<T>, comparing the byref, shape, and strides, not element-wise
public static bool operator ==(SpanND<T> left, SpanND<T> right);
public static bool operator !=(SpanND<T> left, SpanND<T> right);
public static explicit operator SpanND<T>(Array? array);
public static implicit operator SpanND<T>(T[]? array);
// Should we have variants for common ranks (consider 2-5):
// public static implicit operator SpanND<T>(T[,]? array);
public static implicit operator ReadOnlySpanND<T>(SpanND<T> span);
public void Clear();
public void CopyTo(SpanND<T> destination);
public void Fill(T value);
public void FlattenTo(SpanND<T> destination);
public Enumerator GetEnumerator();
public ref T GetPinnableReference();
public SpanND<T> Slice(params ReadOnlySpan<NativeIndex> indices);
public SpanND<T> Slice(params ReadOnlySpan<NativeRange> ranges);
public bool TryCopyTo(SpanND<T> destination);
public bool TryFlattenTo(SpanND<T> destination);
[ObsoleteAttribute("Equals() on SpanND will always throw an exception. Use the equality operator instead.")]
public override bool Equals(object? obj);
[ObsoleteAttribute("GetHashCode() on SpanND 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 ReadOnlySpanND<T>
{
// Should this be in the shape of the following instead:
// public ReadOnlySpanND(void* pointer, nint length, ReadOnlySpan<nint> shape);
// public ReadOnlySpanND(void* pointer, nint length, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
public ReadOnlySpanND(void* pointer, ReadOnlySpan<nint> shape);
public ReadOnlySpanND(void* pointer, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
public ReadOnlySpanND(T[]? array);
public ReadOnlySpanND(T[]? array, int start, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
public ReadOnlySpanND(T[]? array, Index startIndex, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
// Should start just be `nint` and represent the linear index?
public ReadOnlySpanND(Array? array);
public ReadOnlySpanND(Array? array, ReadOnlySpan<int> start, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
public ReadOnlySpanND(Array? array, ReadOnlySpan<Index> startIndices, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
// Should we have variants for common ranks (consider 2-5):
// public ReadOnlySpanND(T[,]? array);
// public ReadOnlySpanND(T[,]? array, ReadOnlySpan<int> start, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
// public ReadOnlySpanND(T[,]? array, ReadOnlySpan<Index> startIndices, ReadOnlySpan<nint> shape, ReadOnlySpan<nint> strides);
public static ReadOnlySpanND<T> Empty { get; }
public bool IsEmpty { get; }
public nint Length { get; }
public int Rank { get; }
public ReadOnlySpan<nint> Shape { get; }
public ReadOnlySpan<nint> Strides { get; }
public ref readonly T this[params scoped ReadOnlySpan<nint> indices] { get; }
public ref readonly T this[params scoped ReadOnlySpan<NativeIndex> indices] { get; }
public ReadOnlySpanND<T> this[params scoped ReadOnlySpan<NativeRange> ranges] { get; set; }
// These work like ReadOnlySpan<T>, comparing the byref, shape, and strides, not element-wise
public static bool operator ==(ReadOnlySpanND<T> left, ReadOnlySpanND<T> right);
public static bool operator !=(ReadOnlySpanND<T> left, ReadOnlySpanND<T> right);
public static explicit operator ReadOnlySpanND<T>(Array? array);
public static implicit operator ReadOnlySpanND<T>(T[]? array);
// Should we have variants for common ranks (consider 2-5):
// public static implicit operator ReadOnlySpanND<T>(T[,]? array);
public void CopyTo(ReadOnlySpanND<T> destination);
public void FlattenTo(ReadOnlySpanND<T> destination);
public Enumerator GetEnumerator();
public ref readonly T GetPinnableReference();
public ReadOnlySpanND<T> Slice(params ReadOnlySpan<NativeIndex> indices);
public ReadOnlySpanND<T> Slice(params ReadOnlySpan<NativeRange> ranges);
public bool TryCopyTo(SpanND<T> destination);
public bool TryFlattenTo(SpanND<T> destination);
[ObsoleteAttribute("Equals() on ReadOnlySpanND will always throw an exception. Use the equality operator instead.")]
public override bool Equals(object? obj);
[ObsoleteAttribute("GetHashCode() on ReadOnlySpanND 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();
}
} |
Because "just" is massively complex, potentially breaking, error prone, and doesn't really solve the problem across the considerations that are actually required. As much as it would be nice if we could "just" fix Span or T[] to use nint, that's basically impossible at this point for all the reasons I detailed in the actual API review. As a summary, it is not pay for play and would regress perf for nearly every app in existence for something which is almost never needed. It introduces new arbitrary failure points for everything that hasn't updated to actually be nint aware yet (with no way to determine what is actually going to work or not). You can't really extend A new type and interface is the simplest/cleanest way to do this. The term tensor is being used because that's more consistent with other ecosystems and the scenarios where you typically need to work with such data. It not being in |
How would the idea of an intermediate base class between all arrays and I don't know why it would be error-prone or massively complex either, unless the runtime code that handles arrays deriving from Anyway, I guess I said enough for this argument, thanks for the discussion, I just hope that the idea that such a change would be massively complex is really verified with the runtime folks. (Unless you work on the runtime as well in which case I apologize for not trusting your judgment enough, it just says you're on the .NET Libraries team in your profile. and from what I've seen in API review videos, runtime costs of various things are always determined by someone like Jan - e.g. "let's add these methods to the class itself unless Jan yells at us" 😄). |
Such a thing doesn't provide any benefit and makes the general type system of .NET more complex with more considerations.
The runtime synthesizing interfaces and types behind the scenes requires languages to understand this and to specially handle that premise, that has cost. It also impacts the cost for dynamic type checks, interface casts, and general complexity of the ecosystem elsewhere. -- There's also, notably, a difference between a new interface and a new concrete base type in terms of considerations/complexity there.
I'm on the libraries team, but due to my areas of ownership in the libraries I'm fairly familiar with the runtime side of things and regularly contribute to the JIT (that's needed to provide the SIMD support for intrinsics or to version the primitive types for example). I do defer to people like Jan (the runtime architecture) and people who are actually on the JIT team in most cases, but this is one of the areas where it's been pretty heavily discussed and so I believe I'm in the generally correct area with the summary I've given here |
Fair enough. It just seems to me like if .NET were designed today, after generics and everything, multi-dimensional arrays would have been designed in a better way, with all of these use cases in mind, and I'm pretty sure someone even mentioned this in this API review - as deriving from some sort of |
Yes, I expect that if we were to design .NET from the ground up there would be a number of changes made to how things were defined/exposed. But, we're on a 20+ year old ecosystem and so backwards compatiblity and not resetting existing source code and/or binaries fully to 0 is important. |
Yes, that's the argument against changing |
I mentioned Jeremy talking about the Span thing simply because the same arguments would apply in favor of it. In case of Span/nint, there are also strong arguments against it - it would break every current for loop that iterates over a span. In this case of arrays, such argument doesn't apply. |
Arrays are in the same bucket with largely the same considerations.
Array is then likewise a GC tracked type, and there are negative implications from having huge allocations, especially if they are known to be used for scenarios like temporaries and therefore are likely short lived. Adding new members can then increase the costs for dynamic type checks and some casts, as there's more types that need to be checked. You do still have to consider the breakage of existing loops since they may miss indices or not be able to represent all indices. We already need to be considerate (due to muscle memory and other bits) that someone may write Beyond that, Having a net new type is an overall "better" direction and allows things to be done in a manner which is more beneficial to the ecosystem long term. |
I guess there was a misunderstanding, I was arguing for multi-dimensional arrays deriving from I do still wonder whether there will ever be a need for single-dimensional |
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);
}
}
namespace System.Numerics.Tensors
{
public ref struct TensorSpan<T>
{
public TensorSpan(T* data, nint dataLength, ReadOnlySpan<nint> lengths);
public TensorSpan(T* 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);
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; }
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);
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);
[EditorBrowsable(EditorBrowsableState.Never)]
[ObsoleteAttribute("Equals() on TensorSpan will always throw an exception. Use the equality operator instead.")]
public override bool Equals(object? obj);
[EditorBrowsable(EditorBrowsableState.Never)]
[ObsoleteAttribute("GetHashCode() on TensorSpan will always throw an exception.")]
public override int GetHashCode();
public ref struct Enumerator
{
public ref readonly T Current { get; }
public bool MoveNext();
}
}
public ref struct ReadOnlyTensorSpan<T>
{
public ReadOnlyTensorSpan(T* data, nint dataLength, ReadOnlySpan<nint> lengths);
public ReadOnlyTensorSpan(T* data, nint dataLength, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public ReadOnlyTensorSpan(ReadOnlySpan<T> span);
public ReadOnlyTensorSpan(ReadOnlySpan<T> span, 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);
public ReadOnlyTensorSpan(Array? array);
public ReadOnlyTensorSpan(Array? array, ReadOnlySpan<int> start, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public ReadOnlyTensorSpan(Array? array, ReadOnlySpan<Index> startIndexes, ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides);
public static ReadOnlyTensorSpan<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 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; }
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);
public void CopyTo(TensorSpan<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);
[EditorBrowsable(EditorBrowsableState.Never)]
[ObsoleteAttribute("Equals() on ReadOnlyTensorSpan will always throw an exception. Use the equality operator instead.")]
public override bool Equals(object? obj);
[EditorBrowsable(EditorBrowsableState.Never)]
[ObsoleteAttribute("GetHashCode() on ReadOnlyTensorSpan will always throw an exception.")]
public override int GetHashCode();
public ref struct Enumerator
{
public ref readonly T Current { get; }
public bool MoveNext();
}
}
public interface IReadOnlyTensor<TSelf, T> : IEnumerable<T>
where TSelf : IReadOnlyTensor<TSelf, T>
{
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> range);
void CopyTo(TensorSpan<T> destination);
void FlattenTo(Span<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> startIndex);
TSelf Slice(params scoped ReadOnlySpan<NRange> range);
bool TryCopyTo(TensorSpan<T> destination);
bool TryFlattenTo(Span<T> destination);
}
public interface ITensor<TSelf, T> : IReadOnlyTensor<TSelf, T>
where TSelf : ITensor<TSelf, T>
{
static abstract TSelf Create(ReadOnlySpan<nint> lengths, bool pinned = false);
static abstract TSelf Create(ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides, bool pinned = false);
static abstract TSelf CreateUninitialized(ReadOnlySpan<nint> lengths, bool pinned = false);
static abstract 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> range);
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);
static TSelf ITensor<T>.Create(ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides, bool pinned);
static TSelf ITensor<T>.CreateUninitialized(ReadOnlySpan<nint> lengths, bool pinned);
static TSelf ITensor<T>.CreateUninitialized(ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides, bool pinned);
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> startIndexes);
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 Tensor<T> Create<T>(ReadOnlySpan<nint> lengths, bool pinned = false);
public static Tensor<T> Create<T>(ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides, bool pinned = false);
public static Tensor<T> CreateUninitialized<T>(ReadOnlySpan<nint> lengths, bool pinned = false);
public static Tensor<T> CreateUninitialized<T>(ReadOnlySpan<nint> lengths, ReadOnlySpan<nint> strides, bool pinned = false);
}
} |
The |
Sorry, I'm late to the party, They look a lot like TensorSpan and ReadOnlyTensorSpan with one major difference: the rank is hardcoded, so I have TensorSpan1, TensorSpan2, TensorSpan3, and so on. This approach has a number of advantages:
Also, I have some interesting methods there: for example, you can cast a Tensor2 to a Tensor3 and back. I've been using this library for a while now, and I've been adding more methods as needed, so there's probably more interesting stuff there. |
Not hardcoding the rank is an explicit design decision and mirrors what most other major tensor frameworks have done. While there are advantages for having something like a |
Yes, I agree, I was not advocating for that, I was just pointing you to yet another tensors library you might pick ideas from, I do a lot of vision computing with onnxruntime, so these are some of the concepts that I came up with that seems to be useful Yes, Tensor2D and Tensor3D would be really welcome for image/vision processing. |
Could we use the new extensions feature in C# 13 for rank-specific operations? For example, matrices and vectors can define the operations as public explicit extension Matrix<T> for Tensor<T> { ... }
public explicit extension Vector<T> for Tensor<T> { ... } This avoids defining new types that either derive from |
Also a bit late to the party, but since I haven't heard it mentioned here nor in the API review videos I'll just ask the question - was |
|
Another reason for using |
Working with strided buffers, however, is notably very expensive and while it works well enough for local experimentation and working with small data sets; it can have significant cost when working with large data sets because it results in effectively random memory access which the CPU or GPU often cannot correctly predict and account for. This is true of all languages that expose the ability to create views in which indices are no longer guaranteed to be linearly sequenced in memory (such as via copy free transpose). |
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
Multi-dimensional Span Types
Tensor Type
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 forNativeTensor<T>
we are looking at ways the users can override this default behavior, it will likely come with someThreadStatic
member that allows users to provide a custom allocator.The text was updated successfully, but these errors were encountered: