You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add support for computing the cumulative sum to the standard #597
This is pretty niche functionality and arguably not "core" enough to implementing an array library for it to be in this standard at all, so I'd vote for leaving it out completely. In a compat layer it could perhaps be named cumulative_* like SciPy did with a few methods, if it's desired for these functions to be there.
This is especially true when one works with statically-shaped system like JAX where these tricks are more or less required. Its usages in the implementation of jax.numpy.nonzero and jax.numpy.repeat are pretty typical. These are also good examples: ex1, ex2, ex3.
This is especially true when one works with statically-shaped system like JAX where these tricks are more or less required.
@soraros it seems like all that usage of cumsum in JAX is just a cumbersome way of working around not having boolean indexing support for in-place ops? And JAX should instead add some other primitive that's more suitable, rather than letting all users manually construct expanded integer index arrays to then use with .at[idx_cumsum].xxx?
# For an array `xs` and a boolean index `conds` of the same shape# example JAX expression for `jax_filter`:>>>xs=jnp.arange(5)
>>>conds=jnp.array([True, False, True, False, True])
>>>cumsum=jnp.cumsum(conds)
>>>cumsumArray([1, 1, 2, 2, 3], dtype=int32)
>>>jnp.zeros_like(xs).at[cumsum-1].add(jnp.where(conds, xs, 0))
Array([0, 2, 4, 0, 0], dtype=int32)
# NumPy:>>>xs=np.arange(5)
>>>conds=np.array([True, False, True, False, True])
>>>xs[conds]
array([0, 2, 4])
>>>np.zeros_like(xs) +np.where(conds, xs, 0) # note: not identical to JAXs `.at`, 0-padding not at the endarray([0, 0, 2, 0, 4])
That kind of JAX code looks very bad, and as a motivator for writing portable code based on a standard I don't think it's a positive. JAX could add support for x[ix_bool] += 1 tomorrow, and imho they should rather than make users write x.at[cumsum(ix_bool)].add(1). It's 100% equivalent. And for the "expanded boolean indexing" where the desired result is [0, 2, 4, 0, 0] as an output, it needs a new builtin function like jnp.bool_index(xs, conds). It has little to do with cumulative sums for data/statistical purposes, it's a type of indexing.
There's a known blocker (brought up by the JAX team before) for adding more in-place operator support beyond what they have now, which is that this standard should be able to better guarantee that += doesn't modify views in NumPy et al. But if we can make that guarantee at some point, then we have what we want here.
@soraros it seems like all that usage of cumsum in JAX is just a cumbersome way of working around not having boolean indexing support for in-place ops?
@rgommers Yes and no. It is a cumbersome workaround, but I also think the problem is more fundamental than that. JAX is essentially a front-end for XLA, and the primitives provided by XLA (for now) require static shape. So the line that actually go wrong is
>>>xs[ix_bool]
array([0, 2, 4])
Note this code does work in JAX, though not jittable, for we don't know its output shape. Let's pretend x[ix_bool] += 1 is syntax sugar for x = x + where(ix_bool, 1, 0) (which works in JAX) for a moment. The same problem appears when we want x[ix_bool] += [1, 3, 5]. Again, we somehow need to know the shape of the rhs, which is equivalent to know the shape of xs[ix_bool] as in the last example.
So what we really work around is the static shape requirement (recall the need of a size parameter for nonzero), which is not exclusively JAX.
Now, for something a bit off-topic.
I think the JAX style functional syntax a = a.at[...].set(...) for in-place operation looks (and arguably works) better than numpy, and I'd really like to have it for array api. Some pros:
Looks familiar, and simulates the feel of in-place operation just fine.
Made it clear nothing is modified. This restricted access pattern would work with any accelerator-backed system. I think it would aid static analysis in system like Numba as well.
More concise, can be chained, and sometimes express our intention better.
a=zeros(m) # initialing aa[I] +=arange(n) # semantically, still initialing a# VS# being concise here is not the important point# this line becomes a "semantical block" for initialisationa=zeros(m).at[I].add(arange(n)) # initialing a
Can specify indexing mode, (more) easily.
# I think these are fairly cumbersome to represent in `numpy`, as we don't have kwargs for __getitem__b=a.at[I].add(val, unique_indices=True) # important info for acceleratorsc=b.at[J].get(mode='fill', fill_value=nan) # sure, we have `take`, but this is uniform and cool
@soraros thanks for all this detail, it's very interesting actually. I think there's something to be said indeed for .at - especially if JAX can make a new cleaner API for it that doesn't rely on things like explicit use of cumsum. This discussion is effectively a follow-up to gh-84 (EDIT: and gh-24). What do you think about opening a new issue to continue this discussion, especially your "for something a bit off-topic" part?
@rgommers Glad you find the exchange interesting! gh-84 is indeed interesting, I will read that throughly later.
I'm not sure how to proceed regarding opening a new issue to continue the discussion though (not sure about the exact topic and/or scope you have in mind). If it's not too much trouble, could you open a new issue so I (or maybe you) can move the relevant part there? Thanks in advance!
I'm not sure how to proceed regarding opening a new issue to continue the discussion though (not sure about the exact topic and/or scope you have in mind). If it's not too much trouble, could you open a new issue so I (or maybe you) can move the relevant part there? Thanks in advance!
Done now in gh-609 - sorry for the delay! I spent a lot of time refreshing my memory and trying to put together something more coherent. But it's tricky; the need for new API in JAX to avoid cumsum seems clear, but if it was easy to exactly define how it'd look, the JAX devs would have done that by now I guess:)
I think cumulative sum is a rather fundamental array operation and we should include it in the standard. None of the typical reasons for omitting a function from the API standard apply here:
It has well defined behavior (aside from NumPy's funny flattening behavior if axis is omitted)
It is not particularly hard to implement in a distributed fashion or on accelerators, as evidenced by how it can be found in Dask and JAX.
It is not easy to implement in terms of other fundamental operations.
To give a few other examples of how I've used it:
Calculating integrals
Calculating moving window averages
Calculating cumulative probabilities
As a reference point on popularity, I see about twice as many uses of np.cumsum in Google's codebase as for np.roll or np.var.
I'll also second Ralf's point on calling it cumulative_sum rather than cumsum.
Reacted by Ralf Gommers, Deepak Cherian, Neil Girdhar, ndgrigorian and Olivier Grisel
Before locking in the specification, it would be worthwhile taking a look at numpy/numpy#6044, and numpy/numpy#14542 that is referenced from numpy/numpy#6044. My comment in numpy/numpy#14542 provides some evidence for the usefulness of allowing the result to include a prepended 0. (More generally, for other cumulative operations such as cumprod, the identity of the operation would be prepended.)
Before locking in the specification, it would be worthwhile taking a look at numpy/numpy#6044, and numpy/numpy#14542 that is referenced from numpy/numpy#6044. My comment in numpy/numpy#14542 provides some evidence for the usefulness of allowing the result to include a prepended 0. (More generally, for other cumulative operations such as cumprod, the identity of the operation would be prepended.)
I agree, starting with zero (and excluding the last value) is quite useful, and I would definitely support adding an optional argument.
Is it clear that this is a better default behavior? My inclination is that it would not be worth the trouble to break existing code.
Is it clear that this is a better default behavior? My inclination is that it would not be worth the trouble to break existing code.
I agree, starting with zero is not a better default behavior, if only because of the long history of the existing behavior in numpy. The links to those previous discussions are more for increasing awareness of the interest and usefulness of this option. That is, don't lock in an API that can't be enhanced with an option later. I don't think that is a problem here, but I don't know how hard it will be to add options to a function in the array API specification later. Also, maybe the reminder will inspire someone to push forward with the enhancement in numpy 🤞 or in other libraries.
Good points on the initial value feature. That doesn't look all that hard to push forward in numpy, there doesn't seem to be a blocker other than no one having done the work.
On the PR there is currently startpoint= and endpoint=, so I am wondering if anyone here has opinions on naming? (Above was start_with_zero= also).
I don't want to include it here, but in principle it makes sense to have initial=value (distinct or included in a start_with_initial=True/include_initial). I am wondering if there is a clearer name for startpoint=True?
On the NumPy issue numpy/numpy#6044include_identity and include_final were brought up. I think I lean towards those names being nicer, include_initial would pair better with an initial= argument for reductions, but not sure it is as clear.
Activity
asmeurer commentedon Feb 15, 2023
There's also
np.add.accumulatewhich may be relevant when looking at the comparison data.rgommers commentedon Feb 15, 2023
Based on previous experience with complaints about bad naming (see, e.g., scipy/scipy#12924 and https://www.reddit.com/r/programminghorror/comments/j6sd61/i_was_just_looking_at_the_documentation_for/) I would very much prefer not to enshrine
cumsumandcumprod(as proposed in gh-598) names in this API standard.This is pretty niche functionality and arguably not "core" enough to implementing an array library for it to be in this standard at all, so I'd vote for leaving it out completely. In a compat layer it could perhaps be named
cumulative_*like SciPy did with a few methods, if it's desired for these functions to be there.soraros commentedon Feb 16, 2023
Niche it may be, I still think
cumsumis pretty useful, as many indexing tricks depend on it. One of such trick is turning "pauses" into "stairs":This is especially true when one works with statically-shaped system like JAX where these tricks are more or less required. Its usages in the implementation of
jax.numpy.nonzeroandjax.numpy.repeatare pretty typical. These are also good examples: ex1, ex2, ex3.rgommers commentedon Feb 16, 2023
@soraros it seems like all that usage of
cumsumin JAX is just a cumbersome way of working around not having boolean indexing support for in-place ops? And JAX should instead add some other primitive that's more suitable, rather than letting all users manually construct expanded integer index arrays to then use with.at[idx_cumsum].xxx?That kind of JAX code looks very bad, and as a motivator for writing portable code based on a standard I don't think it's a positive. JAX could add support for
x[ix_bool] += 1tomorrow, and imho they should rather than make users writex.at[cumsum(ix_bool)].add(1). It's 100% equivalent. And for the "expanded boolean indexing" where the desired result is[0, 2, 4, 0, 0]as an output, it needs a new builtin function likejnp.bool_index(xs, conds). It has little to do with cumulative sums for data/statistical purposes, it's a type of indexing.There's a known blocker (brought up by the JAX team before) for adding more in-place operator support beyond what they have now, which is that this standard should be able to better guarantee that
+=doesn't modify views in NumPy et al. But if we can make that guarantee at some point, then we have what we want here.soraros commentedon Feb 16, 2023
@rgommers Yes and no. It is a cumbersome workaround, but I also think the problem is more fundamental than that. JAX is essentially a front-end for XLA, and the primitives provided by XLA (for now) require static shape. So the line that actually go wrong is
Note this code does work in JAX, though not jittable, for we don't know its output shape. Let's pretend
x[ix_bool] += 1is syntax sugar forx = x + where(ix_bool, 1, 0)(which works in JAX) for a moment. The same problem appears when we wantx[ix_bool] += [1, 3, 5]. Again, we somehow need to know the shape of the rhs, which is equivalent to know the shape ofxs[ix_bool]as in the last example.So what we really work around is the static shape requirement (recall the need of a
sizeparameter fornonzero), which is not exclusively JAX.Now, for something a bit off-topic.
I think the JAX style functional syntax
a = a.at[...].set(...)for in-place operation looks (and arguably works) better thannumpy, and I'd really like to have it for array api. Some pros:Numbaas well.rgommers commentedon Feb 17, 2023
@soraros thanks for all this detail, it's very interesting actually. I think there's something to be said indeed for
.at- especially if JAX can make a new cleaner API for it that doesn't rely on things like explicit use ofcumsum. This discussion is effectively a follow-up to gh-84 (EDIT: and gh-24). What do you think about opening a new issue to continue this discussion, especially your "for something a bit off-topic" part?soraros commentedon Feb 17, 2023
@rgommers Glad you find the exchange interesting!
gh-84 is indeed interesting, I will read that throughly later.
I'm not sure how to proceed regarding opening a new issue to continue the discussion though (not sure about the exact topic and/or scope you have in mind). If it's not too much trouble, could you open a new issue so I (or maybe you) can move the relevant part there? Thanks in advance!
asmeurer commentedon Feb 17, 2023
How do you implement cumulative sum using only array API functions (and without using a Python loop)?
oleksandr-pavlyk commentedon Feb 17, 2023
One inefficient possibility:
[-]Add `cumsum` to the standard[/-][+]Add support for computing the cumulative sum to the standard[/+]rgommers commentedon Mar 9, 2023
Done now in gh-609 - sorry for the delay! I spent a lot of time refreshing my memory and trying to put together something more coherent. But it's tricky; the need for new API in JAX to avoid
cumsumseems clear, but if it was easy to exactly define how it'd look, the JAX devs would have done that by now I guess:)shoyer commentedon Apr 3, 2023
I think cumulative sum is a rather fundamental array operation and we should include it in the standard. None of the typical reasons for omitting a function from the API standard apply here:
axisis omitted)To give a few other examples of how I've used it:
As a reference point on popularity, I see about twice as many uses of
np.cumsumin Google's codebase as fornp.rollornp.var.I'll also second Ralf's point on calling it
cumulative_sumrather thancumsum.WarrenWeckesser commentedon Apr 3, 2023
Before locking in the specification, it would be worthwhile taking a look at numpy/numpy#6044, and numpy/numpy#14542 that is referenced from numpy/numpy#6044. My comment in numpy/numpy#14542 provides some evidence for the usefulness of allowing the result to include a prepended 0. (More generally, for other cumulative operations such as
cumprod, the identity of the operation would be prepended.)shoyer commentedon Apr 3, 2023
I agree, starting with zero (and excluding the last value) is quite useful, and I would definitely support adding an optional argument.
Is it clear that this is a better default behavior? My inclination is that it would not be worth the trouble to break existing code.
WarrenWeckesser commentedon Apr 3, 2023
I agree, starting with zero is not a better default behavior, if only because of the long history of the existing behavior in numpy. The links to those previous discussions are more for increasing awareness of the interest and usefulness of this option. That is, don't lock in an API that can't be enhanced with an option later. I don't think that is a problem here, but I don't know how hard it will be to add options to a function in the array API specification later. Also, maybe the reminder will inspire someone to push forward with the enhancement in numpy 🤞 or in other libraries.
rgommers commentedon Apr 6, 2023
Good points on the initial value feature. That doesn't look all that hard to push forward in numpy, there doesn't seem to be a blocker other than no one having done the work.
arogozhnikov commentedon Jul 8, 2023
Just opinion:
start_with_zero=Trueas kwarg)rgommers commentedon Jul 10, 2023
Looks like there's enough thumbs-ups for cumulative sum at least, and we should add it now. @kgryte just volunteered to open a PR for it.
seberg commentedon Jul 13, 2023
On the PR there is currently
startpoint=andendpoint=, so I am wondering if anyone here has opinions on naming? (Above wasstart_with_zero=also).I don't want to include it here, but in principle it makes sense to have
initial=value(distinct or included in astart_with_initial=True/include_initial). I am wondering if there is a clearer name forstartpoint=True?seberg commentedon Jul 13, 2023
On the NumPy issue numpy/numpy#6044
include_identityandinclude_finalwere brought up. I think I lean towards those names being nicer,include_initialwould pair better with aninitial=argument for reductions, but not sure it is as clear.kgryte commentedon Jul 27, 2023
I've updated the PR to use
include_initial.