-
-
Notifications
You must be signed in to change notification settings - Fork 25.9k
Fix _fill_or_add_to_diagonal
when reshape
returns copy
#31445
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
base: main
Are you sure you want to change the base?
Conversation
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.
Overall looks good.
Co-authored-by: Thomas J. Fan <thomasjpfan@gmail.com>
Co-authored-by: Thomas J. Fan <thomasjpfan@gmail.com>
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.
LGTM. Thanks @lucyleeow
sklearn/utils/_array_api.py
Outdated
@@ -557,6 +557,9 @@ def _fill_or_add_to_diagonal(array, value, xp, add_value=True, wrap=False): | |||
array_flat[:end:step] += value | |||
else: | |||
array_flat[:end:step] = value | |||
# When `array` is not C-contiguous, `reshape` must create |
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.
# When `array` is not C-contiguous, `reshape` must create | |
# When `array` is not C-contiguous, `reshape(array, (-1,))` creates |
Maybe we should go back to using I have a PoC in a branch: https://github.com/lesteve/scikit-learn/blob/4a270381c2c21c715f58923893df87b8c503af0f/sklearn/utils/_array_api.py#L530-L575 The main arguments I have for using
|
Thanks @lesteve ! You make some good points, I'm pretty convinced, especially by your last point. I had a look at your implementation, which is clever but I thought it was a bit confusing with the I created another implementation option and tested the performance your implementation, the original implementation and my new implemenation. Here are the functions in a gist. Summary of the 6 functions tested:
With array (
With transposed array (
I also tried to measure the difference in peak memory usage with numpy with array and transposed array with C-contiguous array:
Transposed F-contiguous array
Performance:
wrap
Maybe we could have 2 separate functions, with Summary:
I have now gone down this rabbit hole way too far 😅 |
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've realised that wrap
is a lot harder to implement in a loop,
I think I would vote for having a minimal implementation, that does not include things that we never use - wrap
and 'repeating' value
(see comment below).
wrap
would be easier to implement than the 'repeating' value, but we currently never use it and maybe I would go with YAGNI. We can always amend, if in some future case, it becomes necessary.
Edit: note that for fill_loop
and add_loop
(in the performance checks above), I've used numpy.fill_diagonal
directly, so current implementation should be slower than that shown above)
if add_value: | ||
array_flat[:end:step] += value | ||
if _is_numpy_namespace(xp): | ||
assignment_function(array.flat, slice(None, end, step), 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.
I've realised that this implementation technically differs from our loop implementation, because if value
was 1D but shorter than the diagonal, value
would be repeated to fill all entries, as is done in numpy see docs:
If array-like, the flattened val is written along the diagonal, repeating if necessary to fill all diagonal entries.
(Though looking at all our use cases, value
should always be the correct size)
With the loop implementation, there is no repeating.
I am tempted to use the loop, even for numpy namespace, just to ensure consistent results...
Test failures are because scikit-learn/sklearn/externals/array_api_compat/common/_linalg.py Lines 203 to 207 in 34e46b0
which does not work for array api strict...? Edit: Ignore this, I did not realise there was a |
array[i, i] = value | ||
else: | ||
for i in range(min_rows_columns): | ||
array[i, i] = value[i] |
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.
array[i, i] = value[i] | |
array[i, i] = value[i] | |
return array |
We use this as x = _fill_or_add_to_diagonal()
so we need to return something (or change how we use it). Otherwise x
ends up being None
.
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 agree. Furthermore, for functions that mutate the values of the array passed as argument, we might want to use "inplace" in their name to avoid bad surprises: _inplace_fill_or_add_to_diagonal
.
Finally, we might want to anticipate the fact that __setitem__
is not supported by jax. The alternative would be to use the .at
operator of array-api-extra
(https://data-apis.org/array-api-extra/generated/array_api_extra.at.html) to anticipate the support of libraries that do not support inplace operations but instead emulate them.
BTW, I wouldn't mind splitting this helper function into two functions:
_inplace_add_to_diagonal
_inplace_fill_diagonal
I think that would help make the callers' code more readable.
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.
So we decided to change this to be in-place because:
It's kind of easy to think that _fill_or_add_diagonal is in-place like np.fill_diagonal and not use the return value. This will mostly work in tests because array will be C-contiguous but will be broken in non C-contiguous cases as we discovered.
(from above)
I was not familiar with numpy diagonal
before, so I can't comment on whether this would be a common mistake that could be made.
I do see that most functions in _array_api.py
do return a value, but I think the original numpy functions also did, unlike np.fill_diagonal
, which is in-place
@lesteve may like to comment?
Reference Issues/PRs
Related https://github.com/scikit-learn/scikit-learn/pull/29822/files#r2101935180
What does this implement/fix? Explain your changes.
When
array
is not C-contiguous (e.g., aftertranspose
),reshape
must create a copy, and cannot return a view. Thus we need to return reshapedarray_flat
, instead of not returning anything. (i.e. we cannot rely on modifying the view, asarray_flat
is not always a view). Also it is advised to avoid mutating a view: https://data-apis.org/array-api/latest/design_topics/copies_views_and_mutation.htmlNote
numpy.fill_diagonal
avoids this problem as it uses.flat
instead ofreshape
I checked that the new test fails with main, but passes with the new changes.
Edit: I guess this didn't get picked up previously with
test_pairwise_parallel
(which checksn_jobs=1
andn_jobs=2
give the same results), as we created the array withorder=F
instead of transposing, which does not seem to cause the same view/copy problem.Any other comments?
@lesteve you were right, it was completely due to the transpose/C->F order change. The failures/passing tests found in
test_pairwise_parallel_array_api
were probably because the numerical instability in euclidean calculation varied. e.g., I think may have passed with array api strict float32 but not float64 just because with lower precision float32, the numerical instability was under tolerance?cc @ogrisel as you reviewed #29822