-
Notifications
You must be signed in to change notification settings - Fork 320
RFC: Comprehensive Plan for Restructuring Dimension Types #1506
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
Comments
@akern40 what's up! thanks for opening this up🚀 about the example repo
about Layout.
useful materials to follow up
|
I don't have much to add :) but in case it is of interest, SciPy is now using mdspan for our special function scalar kernels at https://github.com/scipy/xsf/blob/4fff9b2cb2b5c31a0cf0b0f609d2699a5eeac53b/include/xsf/numpy.h#L68 |
..and I agree with this (from suggestion in ndarry-design)! |
Summary
This RFC proposes a long-term plan for restructuring the library's core types and approach for handling shapes, strides, and dimensionalities. A successful implementation of new dimension types should accomplish the following goals:
isize
, notusize
This RFC is not meant to propose all the changes at once. Rather, this RFC lays out the overall vision and how we will build a smooth transition path. Each step in that path will require its own set of issues and pull requests.
An experimental implementation of this RFC (but not its integration into
ndarray
) can be found here (for theLayout
trait and module definition) and here for the rest of the traits and types. It includes types that implement the traits, additional design notes and rationale, and examples of advanced usage (such as a completely-constant two-by-two matrix shape).cc @bluss @jturner314 @NewBornRustacean @lucascolley
Motivation
Clarity
The current handling of shape and strides in the library is a point of confusion for users (#489, #367, this comment) and a point of complexity for maintainers (#283) since at least 2017. The dimension traits and types in
ndarray
are, at least for me, the most difficult part of the library to understand. I managed to write an entire RFC to revamp the array types themselves without truly understanding the current setup for dimension types. This is a testament to both the complexity of the implementation details and to the power of the current solution.Features
In addition to complexity, the current approach limits some very promising and important optimizations and extensions.
As @grothesque points out in this comment on #1272, fixed-length axes are a critical step for many powerful optimizations. This is an issue that actually led me working on
ndarray
in the first place: I wanted to write code that dealt withNx3
andNx3x3
state vectors and covariance matrices, I wanted that code to be as fast as possible, and I wanted it to be ergonomic to write.Non-strided layouts (blocked, sparse, diagonal, Morton, etc) are also of limited interest; see #321, #1500, and this comment.
Conceptual Explanation
I believe that the core driver of confusion around
Dimension
and its related types and traits is not complexity, but vocabulary. The word "dimension" describes a single size, extent, or measurement. The typeDimension
describes the number of axes an array has, the "dimension" / length of each axis, the stride of each axis, and (sometimes) the index used to access the array; it's even used for helping with iteration. I believe this trait is simply overloaded, both conceptually and practically.Users of the library also often deal with
raw_dim()
, a method which returns the shape as stored within the array. I believe this is emblematic of the vocabulary problem: "dim" here is ambiguous, and not descriptive of the methods actual functionality.In #519, @jturner314 lays out a plan for altering the
Dimension
trait to resolve the shape/strides usize/isize problem. However, discussion at the time suggested that the solution did not go far enough, and that const generics (which have since landed in stable Rust, albeit in a limited form) would likely change the design.So this RFC takes a clean slate approach, inspired partially by the work done for C++
mdspan
and by the excellent work by @fre-hu inmdarray
. However, this proposal is not a direct copy of either approach and was developed independently to supportndarray
specifically.New Vocabulary
The basic conceit of this design is breaking up
Dimension
to give each concept its own trait:Dimensionality
, for the number of axes an array hasShape
, for the lengths of each axisStrides
, for the strides of each axisLayout
, for abstracting the memory order of an arrayStrided: Layout
, a layout specifically for strided arraysThe idea of an "index" - or multiple kinds of indices - likely deserves its own traits or types, but that is a question that can be resolved as the design matures, step-by-step.
Technical Details
Dimensionality
We start by breaking out the concept for "number of axes". I propose a trait,
Dimensionality
, that serves to unify both compile-time numbers of dimensions and runtime numbers of dimensions. Two alternatives deserve mentioning: const generics and typenum. We cannot use const generics universally because they are not yet powerful enough to express the dimensionality for operations like inserting an axis (as that would requireN + 1
to be evaluated in a const context). We cannot use typenum universally because it does not support the dynamic-dimensional "escape hatch" that we need for runtime-only array dimensionalities; plus, its errors are very verbose and may impose extra difficulty in the use ofndarray
. This leaves us with a third option: implement our own type-level numeric system, very akin to typenum, with good const generic support and a dynamic-dimensional escape hatch after a fixed number (e.g., 7 as it is right now or 12, to match the maximum tuple size).A nice helper trait is
Dimensioned
, implemented on types that have a dimensionality (e.g., arrays, shapes, strides, vecs, etc). It provides an associated type,Dimality
(or maybe justNDim
?), that describes how many dimensions it has.This is mostly useful for not always writing
type Dimality: Dimensionality
for all of the following traits.Shape
The next step is to break out the concept of "length of each axis". Here is our first opportunity for an exciting feature enhancement: fixed-length axes, captured at compile time. A basic
Shape
trait can lay the foundation; for exampleI've included some familiar methods (
size
,ndim
,size_checked
) but also some that require explaining:as_slice
returnsCow
because, critically,Shape
does not guarantee that there is an underlying slice to be borrowed, since some axes may have fixed lengths and not be stored at runtimetry_index_mut
, rather than just: IndexMut<...>
, becauseShape
does not guarantee the mutability of any of its axis lengthstry_full
, rather than justfull
, because someShape
s may not allow creation with arbitrary axis lengths (because their sizes are fixed)The natural extension is a
ShapeMut
trait that brings us back to our familiar world of completely mutable shapes.A fun note here: with the
Dimensionality
design, we could even create a trait (AxisMut<N>
) that captures at the type-level that theN
th axis is mutable. I'm not sure this is particularly useful, but I think it's cool that it can be done.Error Handling
I also included a
ShapeStrideError
in the aboveShape
definition; see the reference implementation (linked at the start) for more information, but it's just an enum that implementsDisplay
.Stride
Of course, we'll also be needing a
Strides
trait; this is essentially an exact copy ofShape
, except that theIndex
bound will return anisize
, not ausize
. There would also be an accompanyingStridesMut
for strides that are entirely mutable. Because stride manipulation is pretty central to libraries likendarray
or NumPy, I expect that the bare, immutableStrides
will get a lot less usage than the immutableShape
.I'm not sure whether
Strides
needs to support mixingconst
-valued strides with runtime strides; my initial prototypes don't bother. I can't think of many applications where fixing some subset of strides while allowing others to vary is helpful; but maybe we should! It avoids some complexity, though.Layout
This is where my design proposal really starts to depart from what
ndarray
does right now, and the second opportunity for new features. I'd like to propose aLayout
trait that would serve as the main interface between an array's memory and its "layout".Right now,
ndarray
exclusively supports dense, strided arrays, as theshape
andstrides
are fields within the core types (as of the array reference PR, they sit withinLayoutRef
). However, moving to trait-based shapes and strides means that we'd have to go fromArray<A, D>
toArray<A, D, Sh, St>
forSh: Shape
andSt: Strides
, and that wouldn't allow us to move into capabilities like block-organized matrices, sparse matrices, etc. Here's where theLayout
trait comes in: like the C++mdspan
implementation,Layout
is the gatekeeper between logical indices and memory access:A few things to note:
Shape::as_slice
,Layout::shape
returnsCow
since it does not require the storage of a type that implementsShape
(hyper-optimizations, like fixed-size matrices, may have their shapes and strides defined completely at compile time)Layout
can't implementIndex
, as it requires computing the offset rather than just accessing it in memory; so we defineLayout::index
insteadIndex
associated type, but I'm not sure that's the way to go; or maybe it should have a default?Finally, the
Layout
trait provides a unified interface and vocabulary for users and developers to talk and think about times when an array's shape and strides should be bundled together. The clearest example of this, in my opinion, is the creation of arrays that "look like" another array, in terms of memory order, e.g., azeros_like
function. I believe that doing something likezeros_layout(arr.layout())
gives users a clear way to indicate the shape and strides of a desired array.Strided Layouts
If you're still reading this RFC, you may be so invested as to have noticed that
Layout
doesn't have anything about strides.Instead, I propose we define an additional trait,
Strided: Layout
, as so:This leaves us a path forward for those non-dense and non-strided layouts in the future. We may want to consider restricting
ArrayBase
toL: Strided
either temporarily or permanently as we make the transition to this new design; more on that later.New Generics
Rolling up into
Layout
means we can now go back to our coreArrayBase
(and co.) types using<A, L: Layout>
. This would be a huge breaking change, and we should consider if it's a) worth it and b) how we could make it as smooth as possible. However, it comes with a number of advantages:fn function(arr: &ArrayRef<A, L>) where L: Strided<Dimality = NDim<3>> {}
specifies a 3-dimensional strided arraytype Array3<A> = Array<A, NLayout<3>>
ortype Matrix3x3<A> = Array<A, MatLayout<3, 3>>
Drawbacks
As best I can tell, there are three obvious drawbacks to this approach:
Dimension
approach.Rationale and Alternatives
I think the clearest alternative to this entire plan is to stick with
Dimension
and alter it along the lines of what was suggested in #519. As I said before, while this might help theusize
/isize
problem of shape and strides, I don't think it solves the larger issue of vocabulary, nor does it provide a path for new features. I strongly believe that the pain of breaking changes would be worth the value in ergonomics and features that this new proposal provides.Transition Path
This is a giant proposal that includes major breaking changes. I wanted to get it all down to create a sort of "north star" that library developers can work together to achieve. To actually implement it, I think we'd have to do something like the following:
The transition path itself needs to be fleshed out. However, I think we can start introducing these new traits and types, and start adapting the library. As we go, we'll be able to experiment with the best ways to transition the codebase.
Unresolved Questions
The largest unresolved question is clearly the transition path. However, the interplay of this design with indexing and iteration is still an unknown. I think more experimentation is needed to figure out how
Layout
can ergonomically support iteration, in particular.Summary
This is already a long RFC, so I'll be brief: I propose a design that I think is both more ergonomic and more powerful than our current
Dimension
-based approach and provides users with "industry"-standard vocabulary (i.e., like NumPy or PyTorch or others). Smooth transition will be a challenge, and breaking changes will be involved, but I think it will be worth it.The text was updated successfully, but these errors were encountered: