-
Notifications
You must be signed in to change notification settings - Fork 52
Proposal to standardize element-wise elementary mathematical functions #8
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
Thanks @kgryte. Trigonometry seems fine to me. Regarding rounding:
Regarding special functions: there's a lot more than the four you listed, maybe worth a closer look. I think leaving out all the base-2 and base-10 ones, and things like |
@rgommers Re: Re: special functions. Agreed that there are more. In the OP, I've only included the most universal for the time being with the thought that we could make a follow-up proposal with additional special functions (e.g., |
I compiled generalized signatures (with respect to each of the above listed interfaces for each library) for element-wise elementary mathematical functions, where the raw signature data can be found here. NumPy
CuPy
dask.array
JAX
MXNet
PyTorch
Tensorflow
In short, the minimum common API across most libraries is
For example,
ProposalSignature of the form:
APIs:
NotesOptional arguments as keyword-only arguments for the following reasons:
|
Thanks @kgryte. A couple of thoughts on signatures:
|
@rgommers Thanks! Updated to use keyword-only arguments and removed square brackets (where applicable). |
Just making sure, the keyword |
@oleksandr-pavlyk That's up to us to decide. What I wrote above reflects the "common" API signature across the various array libraries. Perhaps the Torch and Tensorflow contributors can discuss why a Regardless, even if not supported now, we could later update the specification to include a |
It's nice to have, but not used a lot. Given no other library except Dask supports it, I'd suggest it should be left out until that situation changes. |
See #12 for a draft specification. |
Two high level comments:
|
One other longer comment, on the Mutation can be challenging to support in some execution models (at least without another layer of indirection), which is why several projects currently don't support it (TensorFlow and JAX) or only support it half-heartedly (e.g., Dask). The commonality between these libraries is that they build up abstract computations, which is then transformed (e.g., for autodiff) and/or executed in parallel. Even NumPy has "read only" arrays. I'm particularly concerned about new projects that implement this API, which might find the need to support mutation burdensome. ( |
Good suggestion, I'd be in favor. I don't see a big issue with |
We can also keep the keyword-only requirements. In which case, the universal signature would be
|
@shoyer Thanks for the suggestion. I can update the proposals accordingly. |
We talked about this last week, and @alextp said TensorFlow was planning to add mutability and didn't see a real issue with supporting I think removing
Yeah I guess that's the trade-off. SciPy for example has optimizers and linear algebra code where individual iterations are expensive and they're therefore (or for "I'm short on time to Cythonize" reasons) in Python. I don't have a good feeling for how painful this is for TensorFlow/JAX/Dask. I could make an estimate of how bad it'd be for SciPy (gut feeling: not good). @amueller not sure if scikit-learn would have a similar problem? |
Just wanna be careful about language here. These would be a lot slower using NumPy and SciPy, but writing things in an immutable manner does not, a priori, make them slower, as mentioned if you use a jit/whole program optimization. |
It's true that that mutation isn't a major issue in current TensorFlow (v2, with eager mode), but in projects like TensorFlow v1 (explicit graph based execution) mutation is hard. I don't think we want preclude projects like that.
It's worth noting that I suspect that infix operations like |
Let me suggest |
Based on discussion here: #8 (comment)
Just checking: things like |
For tf1 we can support mutation at the python level without supporting mutation at the graph level (think ssa conversion applied between the python program and the graph). |
Along a similar vein, wouldn't supporting mutation at the Python level be generally possible for all array libraries, including future array libraries, regardless of the underlying implementation (graph or otherwise)? It may not always be performant and may involve additional data copying, but this would seem, based on my understanding, straightforward and would not impose an undue burden. |
Additional changes: * Make positional-parameters positional-only (based on discussion here: #8 (comment)) * Document broadcasting behavior * Add document specifying the behavior of the `out` keyword argument * Add explainer for arrays of unequal rank * Add convention regarding shape immutatibility for output arrays * Add data types document
It's definitely always possible to support mutation at the Python level via some sort of wrapper layer. dask.array is perhaps a good example of this. It supports mutating operations and Part of the reason for this is historical -- originally Dask didn't support mutation, aligning its external API with its internal graph based execution model. TensorFlow, JAX and Autograd currently don't support mutation for the same reason. Dask has now relaxed this hard constraint, but some popular NumPy idioms like indexing assignment still aren't supported. I suspect the reason for this is that in some cases it is impossible to do with the efficiency you would expect from NumPy -- the entire computational graph might need to get rewritten, even if only one element of an array is assigned. Rather than adding such performance cliffs, dask doesn't support such assignment at all. The details for PyTorch are different, but it cites similar sounding reasons about rewriting entire graphs for not recommending in-place operations: Going forward, I guess we have a few options:
|
Both 2 and 3 imply that libraries like scipy will still special-case
ndarrays, and they might end up special-casing those arrays by doing
something like 1 anyway. So I mildly prefer that we either do 1 or we
ensure there is adequate coverage of non mutating operators (like jax's
non-mutating slice assign, for example).
…On Wed, Aug 12, 2020 at 6:53 PM Stephan Hoyer ***@***.***> wrote:
Along a similar vein, wouldn't supporting mutation at the Python level be
generally possible for all array libraries, including future array
libraries, regardless of the underlying implementation (graph or
otherwise)? It may not always be performant and may involve additional data
copying, but this would seem, based on my understanding, straightforward
and would not impose an undue burden.
It's definitely always *possible* to support mutation at the Python level
via some sort of wrapper layer.
dask.array is perhaps a good example of this. It supports mutating
operations and out in some cases, but its support for mutation is still
rather limited. For example, it doesn't support assignment like x[:2, :]
= some_other_array.
Part of the reason for this is historical -- originally Dask didn't
support mutation, aligning its external API with its internal graph based
execution model. TensorFlow, JAX and Autograd currently don't support
mutation for the same reason.
Dask has now relaxed this hard constraint, but some popular NumPy idioms
like indexing assignment still aren't supported. I suspect the reason for
this is that in some cases it is impossible to do with the efficiency you
would expect from NumPy -- the entire computational graph might need to get
rewritten, even if only one element of an array is assigned. Rather than
adding such performance cliffs, dask doesn't support such assignment at all.
The details for PyTorch are different, but it cites similar sounding
reasons about rewriting entire graphs for not recommending in-place
operations:
https://pytorch.org/docs/stable/notes/autograd.html#in-place-operations-with-autograd
------------------------------
Going forward, I guess we have a few options:
1. Require support for in-place operations. Libraries that don't
support mutation fully will need to write a wrapper layer, even if it would
be inefficient.
2. Make support for in-place operations optional. Arrays can indicate
whether they support mutation via some standard API, e.g., like NumPy's
ndarray.flags.writeable.
3. Don't include support for in-place operations in the spec. This is
a conservative choice, one which *might* have negative performance
consequences (but it's a little hard to say without looking carefully). At
the very least, it might require a library like SciPy to retain a special
path for numpy.ndarray objects.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#8 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAABHRJBFBAE7F6OURZ5JFLSANBQDANCNFSM4OVJ7ELQ>
.
--
- Alex
|
Recognizing that mutation is still useful, but we are leaning towards not including it in the standard, would the standard provide a recommendation that if the library does support mutation, it should be through the |
I think it's the opposite. The
No mutation at all is extremely limiting. (EDIT: imagine a larger-sized version, to avoid the answer of "just do |
Yeah, if we don't allow That would be my preference, because it requires the least from backends and is easiest to reason about. The semantics for what types of getitem return a view vs which return a copy are subtle, and so it seems easier to just not include any mutation semantics in the spec at all. |
Read up on that at https://jax.readthedocs.io/en/latest/notebooks/Common_Gotchas_in_JAX.html#%F0%9F%94%AA-In-Place-Updates. I understand why JAX needs to do it like that, but it seems like a few bridges too far for the average PyData library author or end user. I'd have a preference for doing
and making JAX add the magic to transform that to
Or otherwise have some exception/optional behavior. Things like |
I agree, it's a bit tricky to do something like this in Dask currently. I'll mention two tricks that often suffice based on my experience working around these types of limitations:
Some version of (2) always works, though it can be tricky to work out (especially with current APIs). The duality between indexing and assignment is the difference between specifying where elements come from or where they end up. |
Perhaps, but if I am using it in my application, what are my options when migrating my code to the pydata_api? If a I would therefore like a standard to recommend, as opposed to require, that population of pre-allocated arrays by element-wise functions, if implemented, be exposed via |
Keep in mind that you're reasoning from a computer science or graph library author point of view. Easier for those stakeholders, way harder for people writing code with the resulting API. Reminds me of people who find Haskell or OCaml easy - if it fits your brain it's awesome, but that's a pretty small fraction of developers.
I think Dask still gets away with it mostly, because it is a container for mutable chunks (NumPy, CuPy), so users don't run into it as often. |
|
We should update that doc page, but I'll just note that JAX has a more recent improved syntax for indexing assignment: One advantage of the non-mutating version is that JAX can have reliable assigning arithmetic on array slices with A disadvantage is that doing this sort thing inside a loop is almost always a bad idea unless you have a JIT compiler, because every indexing assignment operation makes a full copy. So the naive translation of an efficient Python loop that fills out an array row by row would now make a copy in each step. Instead, you'd have to rewrite that loop to use something like |
That is a bit nicer, thanks.
Not sure I understand, if |
Yes, |
I agree that indexing assignment is important, much more important (in my opinion) than an Are we open to "optional" components of the spec? If so, I might just say |
I didn't mean to imply that this is an existing bug, just that it's a little hard to write efficient code like this currently in NumPy. Instead you either have to loop over indices or use the slightly obscure |
In general yes I think, although we have discussed it more in the context of functionality that may be less central to an array library (e.g. an
I'm trying to imagine what that implies for a user. They should then write code like:
But ideally not JAX-specific then - if the special-casing really involves The other problem is that the above example isn't exactly equivalent ( |
After sleeping on it, maybe a better alternative to the above Right now JAX says
which is pretty clear, but I'm thinking it should be (if coming from, e.g., SciPy):
|
Summarized the mutation discussion in gh-24 and added two more options on how to deal with mutation. A dedicated issue seemed useful. |
Mutation discussion is continued in gh-24, |
Based on the analysis of array library APIs, we know that evaluating element-wise elementary mathematical functions is both universally implemented and commonly used. Accordingly, this issue proposes to standardize the following elementary mathematical functions:
Special Functions
Rounding
Trigonometry
Criterion
atan2
).Questions
atan
vsarctan
).The text was updated successfully, but these errors were encountered: