Repository navigation
ENH: support no-copy pickling for any array that can be transposed to a C-contiguous array - #28105
Conversation
|
Hi, I’m new to contributing to NumPy and would appreciate some guidance on the following:
Thank you! |
Yes, see step 5 https://numpy.org/devdocs/dev/index.html#development-process-summary. I'm also not totally sure what the policy is on changing the array pickle format, since people use pickles for uses they are not well-suited but we still need to support those uses anyway... |
seberg
left a comment
There was a problem hiding this comment.
A few comments, we should discuss it but I think we can change it so long we make sure that the new NumPy can read the old pickles (and that also means adding a test that would currently fail, I think).
Also you need a test for the non-contiguous protocol=5 (unless there already is).
|
|
||
| def _frombuffer(buf, dtype, shape, order): | ||
| return frombuffer(buf, dtype=dtype).reshape(shape, order=order) | ||
| def _frombuffer(buf, dtype, shape, order, axis): |
There was a problem hiding this comment.
This function must still be able to deal with old pickles. It is effectively public API.
A test should have existed to find this. We should use pickle.dumps(...) on a current NumPy version, take that string and ensure that pickle.loads(...) on it still works (i.e. freeze the pickle in time).
This looks like it breaks existing pickles and we shouldn't.
There was a problem hiding this comment.
I've given axis a default value None and added a test on a legacy pkl dumped in 2.2.1.
Should I keep this test in code? Or should I move it in test_regression.py?
| if (PyArray_IS_C_CONTIGUOUS((PyArrayObject *)self)) { | ||
| order = 'C'; | ||
| picklebuf_args = Py_BuildValue("(O)", self); | ||
| rev_perm = Py_BuildValue("O", Py_None); |
There was a problem hiding this comment.
Might use PyTuple_Pack() where possible (even refactor).
There was a problem hiding this comment.
I don't get it, you mean construct an empty tuple instead of None to rev_perm on c_contiguous and f_contiguous path?
There was a problem hiding this comment.
I mean I would avoid Py_BuildValue when it is easy to do so, and it is here. But also OK to keep: this is inherited.
There was a problem hiding this comment.
Applied as you suggested.
| for (int i = n - 1, check_s = 1; i >= 0; i--) { | ||
| if (check_s * PyArray_DESCR(self)->elsize != | ||
| items[i].stride) { | ||
| return array_reduce_ex_regular(self, protocol); |
There was a problem hiding this comment.
If we assume it works usually and/or the other path is slow anyway, could also just not check this and see if the transpose works out?
Not sure that is better, though.
There was a problem hiding this comment.
Yeah...Applied, this is much more straightforward!
Not sure if I Py_DECREF correctly. :(
| } else { | ||
| shape = PyObject_GetAttrString((PyObject *)self, "shape"); | ||
| } | ||
| return Py_BuildValue("N(NONNO)", from_buffer_func, buffer, (PyObject *)descr, shape, |
There was a problem hiding this comment.
This isn't right, you lose rev_perm.
There was a problem hiding this comment.
what do you mean lose
There was a problem hiding this comment.
leak? You need to decref it at some point or use N.
There was a problem hiding this comment.
got it, thanks. never wrote on cpython api before, sorry for these oblivious problems.
There is already a test for that numpy/numpy/_core/tests/test_multiarray.py Lines 4372 to 4383 in 94132ac |
seberg
left a comment
There was a problem hiding this comment.
Thanks, the new test looks good. A few more comment, and I have one more comment actually: I wonder if we should just pickle C/F contiguous ones without passing the axes order so that they will still unpickle on older versions (because it is easy, not because it is required).
| } else { | ||
| shape = PyObject_GetAttrString((PyObject *)self, "shape"); | ||
| } | ||
| return Py_BuildValue("N(NONNO)", from_buffer_func, buffer, (PyObject *)descr, shape, |
There was a problem hiding this comment.
leak? You need to decref it at some point or use N.
| x = np.arange(24).reshape(3, 4, 2) | ||
| # generated by same code in 2.2.1 | ||
| data_dir = os.path.join(os.path.dirname(__file__), 'data') | ||
| legacy_pkl_path = os.path.join(data_dir, "legacy_contiguous.pkl") |
There was a problem hiding this comment.
Thanks, this works (I suspect you could use dtype=np.int8 or so and just copy the whole bytes string into the code here also), but just if you like it.
There was a problem hiding this comment.
I used byte str in code and added more examples
| if (PyArray_IS_C_CONTIGUOUS((PyArrayObject *)self)) { | ||
| order = 'C'; | ||
| picklebuf_args = Py_BuildValue("(O)", self); | ||
| rev_perm = Py_BuildValue("O", Py_None); |
There was a problem hiding this comment.
I mean I would avoid Py_BuildValue when it is easy to do so, and it is here. But also OK to keep: this is inherited.
| assert_equal(f_contiguous_array, depickled_f_contiguous_array) | ||
|
|
||
| def test_transposed_contiguous_array(self): | ||
| transposed_contiguous_array = np.random.rand(2, 3, 4).transpose((1, 0, 2)) |
There was a problem hiding this comment.
The transpose example is not chosen carefully, maybe you should just have multiple examples with pytest.parametrize and make at least one 4-5 dimensional so this is less likely to happen:
a = np.ones((1, 2, 3))
a.transpose(1, 0, 2).transpose(1, 0, 2).shape == a.shape
so I don't know from the test if the back-transpose is filled correctly.
There was a problem hiding this comment.
added more test case
|
|
||
| PyObject* shape = NULL; | ||
| if(order == 'K') { | ||
| shape = PyObject_GetAttrString((PyObject *)transposed_array, "shape"); |
There was a problem hiding this comment.
Old messy code, so you can ignore: but we have a PyArray_IntTupleFromIntp helper to use PyArray_NDIM and PyArray_DIMS directly.
There was a problem hiding this comment.
thx, applied
|
ping |
|
Thanks for the ping and sorry for dropping this, I'm going to take another look at this. |
ngoldbaum
left a comment
There was a problem hiding this comment.
Again, sorry for taking so long to get back to this.
This is looking much better than the last time I reviewed, thanks for the careful updates.
I'm approving but I want another experienced developer to look this over, since changing the pickle format has annoying backward compatibility concerns associated with making any mistakes.
@seberg or maybe @mhvk would either of you be interested in giving this another pass?
seberg
left a comment
There was a problem hiding this comment.
Sorry for forgetting about this (it was great that you made many of the other smaller cleanups here!).
I think it's good, I have one substantial ask, unfortunately. Because I really think we should try to allow old NumPy versions to load most pickles and I don't think this does.
|
seems something's wrong with freebsd's ci |
|
@IndifferentArea don't worry about it. But if you would like to see it green, try merging/rebasing the main branch. That would likely fix it. |
|
Thanks @IndifferentArea |
|
@charris @seberg This PR introduced some linting errors on my local system. For example in numpy/_core/tests/test_multiarray.py line 4452 a trailing whitespace. The CI was green for the PR, in particular the lint test: https://github.com/numpy/numpy/actions/runs/14085614702/job/39448799495. Not sure what happened here. The ruff version used in the CI is a bit older, but on my system it gives the same errors. I will create a PR to update ruff and address the trailing whitespace. |
|
It probably needed a rebase before merging. |
|
Awesome to see it merged! |
… a C-contiguous array (numpy#28105) * add support for transposed contiguous array and corresponding test * fix lint * rm bad includes * fix * fix * discard vla * accept legacy pkl * check return * fix ci * check after transpose * discard auto include * use N format in pybuildvalue * use pytuple pack * clean up * add comment on fall back behavior * add mroe test * use byte str instead of pkl to test * get shape with pyarray api * fmt * better comment * fix as suggested * fix lint * discard auto include * shorter tested strs * fmt * last correct commit * last correct commit * [skip ci] memory leak * add cleanup * [skip ci] add release doc * [skip ci] add check for pylong_fromlong * typo * fix memory leak * more clean up * use same random seed * use xdecref * check PyArray_Transpose * check PyArray_IntTupleFromIntp * fix * once-over * add additional test * once over again * fmt * pickle format consistency * fix incorrect usage of cpy api * fmt
As discussed in #26878, support no-copy pickling for any array that can be transposed to a C-contiguous array.