Skip to content

ENH: Adding Object dtype to einsum#18053

Merged
seberg merged 11 commits intonumpy:mainfrom
i-am-b-soto:adding_object_to_einsum
Apr 28, 2023
Merged

ENH: Adding Object dtype to einsum#18053
seberg merged 11 commits intonumpy:mainfrom
i-am-b-soto:adding_object_to_einsum

Conversation

@i-am-b-soto
Copy link
Copy Markdown
Contributor

@i-am-b-soto i-am-b-soto commented Dec 22, 2020

Hi Numpy,

Following issue #17837 I'm attempting to add support for the Object dtype to einsum.

Currently, I'm encountering an issue with scalar types.
If we take, for example

a = np.arange(1, dtype="object")
assert_equal(np.einsum("i,i", a[1:], a[:-1], optimize=do_opt), np.dot(a[1:], a[:-1]))

This doesn't work because einsum produces False were dot produces None. You can view this scenario in action in numpy/core/tests/test_einsum_object.py line 245. test_einsum.py line 539. I'm not sure how to resolve this.

This is my most ambitious contribution to Numpy to date and I very much appreciate any and all advice, and your patience! Thank you!

@i-am-b-soto
Copy link
Copy Markdown
Contributor Author

Also,
Right now there is just one function used to handle all the optimizations of einsum. (It's based on _sum_of_products_any) If I can get this to pass the tests and get a green light from other members of the team, I can add more of the optimizations.

@@ -0,0 +1,309 @@
import itertools
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the test case are just copied from test_einsum.py, which I think is unnecessary, you can just add object type to test_einsum.py.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's another issue I got stuck on.

The tests in check_einsum_sums do somethig like:

a = np.arange(1, dtype='object')
np.sum(a, axis=-1).astype('object'))

which raises _`AttributeError: int' object has no attribute 'astype'
I guess a object array with size =1 gets converted to a regular python int at some point in np.sum()
So, I needed a test to remove the .astype

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could always add a special case within the test_einsum.py tests for when dtype=object, to avoid those troublesome tests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could always add a special case within the test_einsum.py tests for when dtype=object, to avoid those troublesome tests.

Done

Comment on lines +539 to +543
if(PyErr_Occurred()){
return -1;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think calling PyErr_Occurred is safe from within NPY_BEGIN_THREADS_THRESHOLDED, but hopefully someone who remembers more about the threading (AKA releasing the GIL) can review to confirm or refute.

I suspect the real error here is that NPY_BEGIN_THREADS_THRESHOLDED is not safe to call at all when handling object arrays.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't consider this.
Perhaps calling sop can be split into two functions based on type_num. one function releases the GIL were the other does not, and checks for PyError_Occured.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See also gh-18450 for how the error should be checked here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the problem of API calls inside the NPY_BEGIN_THREADS macros is resolved: it will only release the GIL and check error conditions if the needs_api is False.

Base automatically changed from master to main March 4, 2021 02:05
@ketch
Copy link
Copy Markdown

ketch commented Mar 28, 2021

I'm just here to say that this enhancement would be extremely useful to me. Please keep at it!

@i-am-b-soto
Copy link
Copy Markdown
Contributor Author

Following @eric-wieser 's comment on PyError_Occured not being allowed inside NPY_BEGIN_THREADS_THREASHOLDED How does the team feel about defining a new macro.
Something like:

#define NPY_BEGIN_THREADS_DESCR_THRESHOLDED(loop_size, dtype) do { if ((loop_size) > 500 && (!(PyDataType_FLAGCHK((dtype), NPY_NEEDS_PYAPI)))) \
{ _save =PyEval_SaveThread();} while(0); 

Which just combines NPY_BEGIN_THREADS_DESCR with NPY_BEGIN_THREADS_THREASHOLDED
And then is called inside einsum.c.src like:
NPY_BEGIN_THREADS_DESCR_THRESHOLDED(shape[1] * shape[0], NpyIter_GetDescrArray(iter))

I can get to work on drafting some tests for this

@seberg
Copy link
Copy Markdown
Member

seberg commented Mar 30, 2021

I am happy with adding it (probably a few places that use the other should be using that anyway), although I am not sure that here relying on int needs_api = NpyIter_IterationNeedsAPI(iter) isn't just as well.

I personally would like if we returned -1 on error from the sop (but that would also mean adding return 0; to all others).

@ketch
Copy link
Copy Markdown

ketch commented Sep 13, 2021

I'm here again to say that I would absolutely love to have this functionality in numpy. Is there anything I could do to help out?

@seberg
Copy link
Copy Markdown
Member

seberg commented Sep 13, 2021

@ketch, I think this PR is pretty much stalled at this point. It could probably be picked up and start a new one with some inspiration from here (in that case preferably with the commits as basis for attribution unless).

@InessaPawson InessaPawson added the 52 - Inactive Pending author response label Aug 8, 2022
@mattip mattip force-pushed the adding_object_to_einsum branch from e3813b7 to 31e2176 Compare March 30, 2023 08:29
@mattip
Copy link
Copy Markdown
Member

mattip commented Mar 30, 2023

Also closes #23492

@seberg
Copy link
Copy Markdown
Member

seberg commented Mar 30, 2023

@mattip did you fix this up more generally?

Can we fix PyArray_AssignZero instead since it seems to be a private function to begin with? The concept of zero filling is used in some other places. This is OK for now, but we need to find a more generic solution eventually (this doesn't generalize to more DTypes). I think np.zeros actually uses int(0) and then uses force-casting, which may be the more generic solution here (with a fast-path that we can keep to assume that non-reference object memset is fine, but that may also not generalize perfectly).

Can also do push it off, but I hadn't even realized that there was this place that has another version of custom zero filling...

@mattip
Copy link
Copy Markdown
Member

mattip commented Mar 30, 2023

PyArray_AssignZero is currently used in exactly one place (in einsum). There is a comment at the top of reduction.h that seems to be left over from a previous refactoring. I will

  • refactor the function to handle all dtypes, including user-defined
  • use it in PyArray_MatrixProduct2

@mattip mattip force-pushed the adding_object_to_einsum branch from 6317a13 to d1306cb Compare March 30, 2023 13:03
@mattip mattip removed the 52 - Inactive Pending author response label Mar 30, 2023
@mattip
Copy link
Copy Markdown
Member

mattip commented Mar 30, 2023

I removed the WIP and added the ENH label. Does this need a release note?

@seberg
Copy link
Copy Markdown
Member

seberg commented Mar 30, 2023

I think its worth an improvement note, has been asked for often enough!

@seberg seberg changed the title WIP: Adding Object dtype to einsum ENH: Adding Object dtype to einsum Mar 30, 2023
@mattip
Copy link
Copy Markdown
Member

mattip commented Mar 30, 2023

In the top comment, @iamsoto mentioned:

This doesn't work because einsum produces False where dot produces None

It turns out both were wrong: the correct value is 0 in both cases. This was due to PyArray_AssignZero assigning False for object dtype, and dot improperly using memset for object dtype arrays.

``np.einsum`` now accepts arrays with ``object`` dtype
------------------------------------------------------
The code path will call python operators on object dtype arrays, much
like ``np.dot`` and ``np.matmul``.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be an assumption that it is OK to start with int(0), that I am not sure is shared by all code paths (e.g. dot does not). I can live this this, though, even if it might be considered an issue.

Anyway, have to review the rest carefully, but not today.

@mattip
Copy link
Copy Markdown
Member

mattip commented Apr 7, 2023

I think this is ready for review.

Copy link
Copy Markdown
Member

@seberg seberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GIL releasing doesn't seem right. Otherwise, it would be good to clean up the function name duplication a bit mostly I think.

NPY_BEGIN_THREADS_THRESHOLDED(shape[2] * shape[1] * shape[0]);
for (coords[1] = shape[2]; coords[1] > 0; --coords[1]) {
for (coords[0] = shape[1]; coords[0] > 0; --coords[0]) {
sop(2, ptrs[0], strides[0], shape[0]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sop(2, ptrs[0], strides[0], shape[0]);
sop(2, ptrs[0], strides[0], shape[0]);

if really would be nicer if sop would just have a return value IMO. Then we could avoid the whole PyErr_Occurred() thing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened #23671 for this

* loop provided by the iterator is in Fortran-order.
*/
int needs_api = NpyIter_IterationNeedsAPI(iter);
NPY_BEGIN_THREADS_THRESHOLDED(shape[2] * shape[1] * shape[0]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NpyIter_IterationNeedsAPI is OK for now. But we have to use it for more than the error guard. Can't release the GIL (and have to restore, although that may not need a conditional, since the macro's should come with one).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(It would be nice to have a test with large enough inputs to make sure the GIL release path would be taken.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copy-pasted the pattern from elsewhere. I will check for needs_api before calling NPY_BEGINS_THREADS_THRESHOLDED everywhere in the file. Indeed, NPY_END_THREADS does not need a guard, it checks whether NPY_BEGINS_THREADS_THRESHOLDED actually released the GIL before re-acquiring it.

* object_sum_of_products_outstride0_one,
* object_sum_of_products_outstride0_two,
* object_sum_of_products_outstride0_three,
* object_sum_of_products_outstride0_any#
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of templating here. Notes:

  • Not all of these are ever used (The code coverage may actual points these out correctly)
  • The function picking the specialization accepts NULL as "not implemented" in some but not all places (which could be extended)
  • I don't mind aliasing some, but its all in one file how about just doing #define ... object_sum_of_products_any? and avoid the templating?

@mattip mattip force-pushed the adding_object_to_einsum branch from f7c9f71 to 962120b Compare April 27, 2023 20:32
@mattip
Copy link
Copy Markdown
Member

mattip commented Apr 27, 2023

reguide_check crashed but it passes locally.

}
Py_SETREF(prod, PyNumber_Multiply(prod, curr));
if (!prod) {
return;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have thought this is covered by a test I added with NULL pointers?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be multiplication raising. string objects would do?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, but NULL filled would be None, so it should be covered, weird, but OK.

PyObject *sum = PyNumber_Add(*(PyObject **)dataptr[nop], prod);
Py_DECREF(prod);
if (!sum) {
return;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I can get this line covered with an object that returns itself on multiplication and raises on addition?

@seberg
Copy link
Copy Markdown
Member

seberg commented Apr 28, 2023

The coverage is just weird, but probably largely due to the odd templating and the function being instantiated way too many times. It seems overall OK, though.

We do start the reduction with int(0) for object, which I think should be OK in for most practical purposes (and if anyone wants to change it, it should be plausible to do).

I am going to merge this, there are two cleanups that really should happen:

  • The loop duplication is just a mess, maybe just better about NULL meaning fall-through would be good (lets not care much about a few misses for object loops).
  • Error returns relying on PyErr_Occurred() is something I spend a lot of work on getting rid off, and I don't like this making almost a step backwards (before, it didn't matter).

Tests seem fine, and it is nice that there is the dot fix here! So lets give this a shot.

Thanks @iamsoto and especially Matti for the many follow-ups!

@i-am-b-soto
Copy link
Copy Markdown
Contributor Author

@seberg
I'm sorry, I haven't been able to look at this for some time. Really happy you guys got it figured out!!

BvB93 added a commit to BvB93/numpy that referenced this pull request May 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants