Skip to content

BUG,ENH: Fix generic scalar infinite recursion issues #26904

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

Merged
merged 7 commits into from
Jul 24, 2024

Conversation

seberg
Copy link
Member

@seberg seberg commented Jul 10, 2024

This reorganizes the avoidance of infinite recursion to happen
in the generic scalar fallback, rather than attempting to do so
(badly) in the scalarmath, where it only applies to longdouble
to begin with.

This was also needed for the follow up to remove special handling
of python scalar subclasses.

Closes gh-26767

seberg added 3 commits July 10, 2024 16:54
This reorganizes the avoidance of infinite recursion to happen
in the generic scalar fallback, rather than attempting to do so
(badly) in the scalarmath, where it only applies to longdouble
to begin with.

This was also needed for the follow up to remove special handling
of python scalar subclasses.
res = PyArray_GenericBinaryFunction(m1, other_op, n_ops.power);
}
else {
res = PyArray_GenericBinaryFunction(other_op, m2, n_ops.power);
Copy link
Member Author

Choose a reason for hiding this comment

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

I started calling the ufunc directly now. This has advantages and disadvantages, but overall: I don't think it should matter much and besides working around power oddities felt pretty sane.

(Not done yet for the comparison, where it might also make sense eventually)

@seberg
Copy link
Member Author

seberg commented Jul 10, 2024

I don't care for backporting this. The bug has been around forever.

# As of NumPy 2.1, longdouble behaves like other types and can coerce
# e.g. lists. (Not necessarily better, but consistent.)
assert_array_equal(op(sctype(3), [1, 2]), op(3, np.array([1, 2])))
assert_array_equal(op([1, 2], sctype(3)), op(np.array([1, 2]), 3))
Copy link
Member Author

Choose a reason for hiding this comment

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

So yes, the new paths aligns longdouble with others here. I could guess at this not being right, that would remove the array conversion step.

(It may mean deciding that one test in gh-26905, just can't pass, because we simply cannot deal with subclasses of Python floats even if they get converted to a float array later. Although, it can go either way, since you can still convert but only allow the was_scalar path.)

@seberg seberg added this to the 2.1.0 release milestone Jul 12, 2024
@ngoldbaum ngoldbaum self-requested a review July 13, 2024 15:34
Copy link
Member

@ngoldbaum ngoldbaum left a comment

Choose a reason for hiding this comment

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

I don't have a lot of context for the details here, but I think removing special cases for longdoubles makes a lot of sense, and the logic behind the retrying for object arrays is explained in a lot of detail.

Sorry for taking so long to look at this.

/* self_item can be used to retry the operation */
*self_op = self_item;
return 0;
}
Copy link
Member

Choose a reason for hiding this comment

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

don't you leak self_item if you get to this last return 0? Maybe also in the case where you assign it to self_op, I haven't looked at how reference counting works in the caller.

* Generally, we try dropping through to the array path here,
* but this can lead to infinite recursions for (c)longdouble.
* We drop through to the generic path here which checks for the
* (c)longdouble infinite recursion problem.
Copy link
Member

Choose a reason for hiding this comment

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

Might make sense to just refer to this as the "infinite recursion problem", without special casing longdouble anymore. Maybe also leave a gh-XXXX reference.

Copy link
Member Author

Choose a reason for hiding this comment

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

For this file only longouble is problematic, but fair, removed it and added some references.

@@ -875,8 +876,8 @@ def test_operator_object_left(o, op, type_):


@given(sampled_from(objecty_things),
sampled_from(reasonable_operators_for_scalars),
sampled_from(types))
sampled_from(reasonable_operators_for_scalars + [operator.xor]),
Copy link
Member

Choose a reason for hiding this comment

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

Does stuff break if you add xor to reasonable_operators_for_scalars? If not i'd just add it there. If so, maybe a comment explaining why only xor here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll think a bit if I can think of a change. I think that would be a rename of the variable and not a comment here.

@ngoldbaum ngoldbaum added the triage review Issue/PR to be discussed at the next triage meeting label Jul 22, 2024
@ngoldbaum ngoldbaum merged commit 7d6973a into numpy:main Jul 24, 2024
68 checks passed
@ngoldbaum
Copy link
Member

We chatted about this at the triage meeting and I got some extra context about why we're doing this.

@seberg seberg deleted the scalar-recursion-fix branch July 24, 2024 18:29
@mroeschke
Copy link
Contributor

mroeschke commented Aug 5, 2024

I think this broke a use case in pandas where scalar arithmetic should have delegated to __array_ufunc__?

In [3]: class Array:
   ...:     def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
   ...:         return 1
   ...: 

In [4]: np.timedelta64(120,'m') + Array()
Out[4]: 1

In [5]: np.__version__
Out[5]: '1.26.4'
In [3]: class Array:
   ...:     def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
   ...:         return 1
   ...: 

In [4]: np.timedelta64(120,'m') + Array()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 np.timedelta64(120,'m') + Array()

TypeError: unsupported operand type(s) for +: 'datetime.timedelta' and 'Array'

In [5]: np.__version__
Out[5]: '2.1.0.dev0+git20240802.0469e1d'

@seberg
Copy link
Member Author

seberg commented Aug 5, 2024

Hah, yes it defers still, but if you don't implement __radd__ then the ufunc is never called now. Hmmm...

@seberg
Copy link
Member Author

seberg commented Aug 6, 2024

OK, there are two problems, here (the second is even more subtle I guess):

  1. If the other object has the __array_ufunc__ attribute, we promise that the ufunc is called (unless we already deferred because it is None). It might be nice to incorporate that into the macro, but that seems more tedious than worth it.
    Basically, the implementations all work on the assumption that if we don't defer, we quickly end up calling the ufunc. This PR broke that assumption partially because that is necessary to break recursion.
  2. There is a subtle issue that converting the array early might lose the __array_wrap__ information. I doubt there is a great solution for that, since I don't want to duplicate logic. Since this is already fast-path for most cases, not sure if there is a need to worry about the "unnecessary" array conversion.

@seberg
Copy link
Member Author

seberg commented Aug 6, 2024

OK, gh-27117 should fix 1. I looked at 2. a bit closer and I think it's not worth worrying about it, because the only way one could think it matters is for things that have the exact scalar array-priority, which is weird.
Otherwise, I suppose it could matter for scalar subclasses that define a priority, but we never supported that anyway. (Which seems fine to me: subclassing them is bound to create inconsistencies all over the place anyway.)

@mroeschke
Copy link
Contributor

At least from the pandas side, issue 2. shouldn't be significant since there's only one use of __array_wrap__ which doesn't have significant logic to it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
00 - Bug triage review Issue/PR to be discussed at the next triage meeting
Projects
None yet
Development

Successfully merging this pull request may close these issues.

BUG: Segmentation fault when doing a "Bitwise OR" on a np.longdouble with a None value
3 participants