-
Notifications
You must be signed in to change notification settings - Fork 297
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
Implement co-broadcasting in operator overloading #898
Conversation
src/numeric/impl_numeric.rs
Outdated
where | ||
A: Clone + Zero + FromPrimitive + Add<Output = A> + Div<Output = A>, | ||
D: RemoveAxis, | ||
D::Smaller: BroadcastShape<Ix0>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can make a specific comment here - but it is more important to talk about the whole picture in this pull request.
Specifically, this new bound
-
Looks like
mean_axis
is suddenly exposing unnecessary implementation details - we don't want to make any externally visible changes to this method's signature - what is required to keep the old signature? -
This is, to me, a sign that users will have to use this kind of bound in their dimension-generic array code. I think that makes ndarray harder to use, even if it's more "powerful". Objectively that looks like a blocking concern for this feature. I mean, if the answer to (1) is "don't use arithmetic ops", then I have reservations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good point and something I hadn't considered before. Unfortunately, I don't think there's a way to avoid the extra bound without improvements to the language (either specialization or enum-variants-as-types), since the compiler needs to know that BroadcastShape
is implemented for all possible combinations of dimension types to remove the bound.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's see how far we get with the blanket implementation for all Dimension
with output Self
and then maybe it can be added impls that show that Ix0
can broadcast to any other dimension.
Already now we need to think about "min const generics" which hit stable soon. What can we preserve when transferring over to const generics for const dimensions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't think of a good way :( If we add a statement like this:
impl<D: Dimension> BroadcastShape<Ix0> for D {
type BroadcastOutput = D;
}
Then it will conflict with the statement on broadcast.rs:62( Although they don’t actually conflict ).
Co-broadcasting is cool and interesting! I can see that you are attacking our fixme's in code one by one and that's gratifying, but it doesn't always tell the whole story of what might be the most important issues to solve. (*) Edit: On further thought the code example below might just work out of the box - that would resolve most of this :) I think the type system is still limited in what can be expressed, and we seem to have a typenum-like problem here (not really the fault of typenum, just the limits of the type system). Expressing one generic arithmetic operation is ok.. but what about two? Will it be usable for the user? Imagine this: type Mat<D> = Array<f64, D>;
fn foo<D>(x: Mat<D>, y: Mat<D>, z: Mat<D>) -> Mat<D> {
x + &y * &z
} How would we write the actual return type of the function? Maybe in some cases this change makes it harder for the user of ndarray, or can we find a great solution? It could be that co-broadcasting should be a feature that ndarray implements, without using it by default in the arithmetic ops. (*) Specifically see the issue about #699 - there are lots of things we want to spend the - implementation, complexity, etc - budget on in arithmetic ops. |
If the dimension types of the parameters are all D, the result can also be written directly as D. But if the dimensions are not the same, it will be really troublesome. That's right, if the cost of using operators is that additional restrictions must be added, it maybe better to risk panic. I'm sorry I can't think of a better way for the time being. Thanks for pointing out. |
Changed the title from "fix" since it's a new feature, not a fix. There is more feedback and thoughts to be given, when I have time. 🙂 |
Here's a trick that actually works to resolve the What we do here is that we put the See this commit on a temporary branch for the diff to this PR: 54c3a85 |
That's a really nice solution. Is it possible to add Edit: I wonder if we could also add |
That is amazing! It's so surprising it works! However, we still can’t add statements like
And it did run successfully in my nightly version (cargo 1.49.0-nightly (d5556aeb8 2020-11-04) ), I think we can achieve this amazing work as soon as the 1.49 version is stable. [Edit]I updated my rust version and found that the stable version 1.49 has been released, and it does works. But the CI of ndarray still seems to be version 1.42. On the other hand, such changes may affect users of previous rust versions, just like me lol |
Note that |
We could also add this bound to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry I haven't at this time provided a high-level plan
I used uninitialized() to create an uninitialized array, and added the zip_mut_from_pair method to impl_methods.rs to get the result. Now no matter whether we need co_broadcast or not, we only need to traverse self and rhs once each. And ArcArray will not be turned into Array. But there is still a problem: both uninitialized() and maybe_uninit() methods require elements to implement the copy trait (I don’t know the reason behind this). So it is still unavoidable to change the signature of the method in impl_numeric.rs. |
uninitialized is hard to use correctly. I haven't removed it yet because I think I have managed to put it on sound ground - it is technically possible to use it correctly. See issue #804 and issue #876 (we have just completed removing all internal usage of this method). Unfortunately
Please use |
src/dimension/broadcast.rs
Outdated
|
||
pub trait BroadcastShape<Other: Dimension> { | ||
/// The resulting dimension type after broadcasting. | ||
type BroadcastOutput: Dimension; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A trait with just one associated type - an "output type" (See RawDataSubst and FnOnce as inspiration) should almost always prefer the associated type name "Output" as a matter of convention. Think of it as a type-level function's return value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks much for teaching. I changed BroadCastOutput to Output
src/impl_methods.rs
Outdated
S1: Data<Elem = B>, | ||
S2: Data<Elem = C>, | ||
F: Fn(&B, &C) -> A, | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just use Zip
in this method (is it slower?)? Zip::map_assign_into
should do it (assign into self).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, we can use Zip. This function does not need to be added.
I tried to use maybe_uninit(), but I can’t think of a simple way to generate an ArrayBase with the data type MaybeUninit<A>. So I added a trait named MaybeUninitSubst to get the mapping from ArrayBase with element A to MaybeUninit<A>. In addition used Zip to get the results. |
Good fighting. The type related problems with the maybe_uninit constructor are not so fun, we're really feeling that we miss the "GAT" feature here (still experimental). |
I'm working on fixing the maybe uninit functionality so that it can do what we need. |
Cool! It will be very exciting if it and co_broadcast both land. |
e2d8008
to
23342b1
Compare
@bluss Thank you so much for the amazing work! I have rebased the pr and used map_collect_owned according to your guide to avoid unsafe code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's starting to getting in shape, we've managed to reduce the arithmetic ops trait requirements to a minimum (optimal would be to have all dimensions implement BroadcastShape<E>
but it seems impossible and this is acceptable for now).
Arithmetic ops and broadcasting docs (on ArrayBase) look like they need an update
src/dimension/broadcast.rs
Outdated
/// The resulting dimension type after broadcasting. | ||
type Output: Dimension; | ||
|
||
/// Determines the shape after broadcasting the dimensions together. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shapes, not dimensions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Determines the shape after broadcasting the dimensions together. | |
/// Determines the shape after broadcasting the shapes together. |
src/impl_ops.rs
Outdated
let shape = self.dim.broadcast_shape(&rhs.dim).unwrap(); | ||
let lhs = self.broadcast(shape.clone()).unwrap(); | ||
let rhs = rhs.broadcast(shape).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this to perform well we need to ensure that no-ops pass through efficiently. Maybe it can be a task for another time. The existing .zip_mut_with
solution it only broadcasts if necessary, so it can be a lot more efficient. Let's fix that after everything else - not because it's less important but because it makes the commit history easier to understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's right. But the method to convert the dim of ArrayBase seems to have only into_dimensionality
and broadcast
. However, the former cannot be used for reference types. I think we can add the judgment whether the shape is the same in broadcast
.
src/dimension/broadcast.rs
Outdated
// Uses the [NumPy broadcasting rules] | ||
// (https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html#general-broadcasting-rules). | ||
// | ||
// Zero dimension element is not in the original rules of broadcasting. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which original rules? ArrayBase::broadcast
seems concsistent with these rules already. Also, we need to ensure here that we do exactly the same thing as in ArrayBase::broadcast
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not find a description of the zero value in the Numpy broadcast rules cited above. However, if there is no difference between a zero value and a value greater than 1, I think this explanation can be omitted.
ArrayBase::broadcast
only detects the shape mismatch, and does not make any changes to dim.
*x = x.clone() $operator y.clone(); | ||
}); | ||
self | ||
if self.ndim() == rhs.ndim() && self.shape() == rhs.shape() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't reviewed the PR in detail, but I just wanted to make a small comment here. There's another case that's worth considering: when it's possible to broadcast the RHS to match the existing shape of the LHS. In this case, it's possible to reuse the existing LHS allocation (like in the current release of ndarray
and the if
branch here), instead of collecting into a new array (like the else
branch here).
This observation also applies analogously to the other ops impl with DataOwned
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is room for future improvement.
I'll just make a list of "known issues" that we can fix after the PR review and merge
|
9f8909e
to
0dbeaf3
Compare
@bluss It has been modified accordingly. I very much look forward to our ability to solve the above problems in the future. Especially removing the restriction like |
…stOutput to Output, remove zip_mut_from_pair()
I've rebased this to fix the conflict (sorry about that). Could we update the PR description for the current state of the PR before merge? |
For consistency with other dimension traits (to come); with <A as DimMax<B>>, Output is the maximum of A and B.
While calling co_broadcast directly is less convenient, for now they are two different functions.
OK, it has been modified accordingly |
Thanks :) |
This PR did the following to implement co_broadcasting in operator overloading