Conversation
6f02c65 to
c29564a
Compare
c29564a to
30dc6e9
Compare
|
Thanks for the PR and for looking into this. As I understand this tries to add version specific condition compilation similar to Additionally Python12 (and numpy as well) is slowly moving towards a new stable abi build around opaque So currently I'm inclined to say we should keep the current design. cc @davidhewitt @ngoldbaum Maybe you have some opinions/comments for this one as well. Footnotes |
ngoldbaum
left a comment
There was a problem hiding this comment.
I've personally been responsible for adding new structs and functions to the NumPy C API and it would be a shame if those functions can't be used by rust-numpy.
That said, this does introduce a significant amount of complexity. I also don't think it makes much sense to try to support very old versions of NumPy.
I would instead try to support only NumPy 1.25 and NumPy 2.1 and newer. I would also tend not to care much about supporting NumPy 1.x but I understand if that's important for people.
| pub legacy_inner_loop_selector: PyUFunc_LegacyInnerLoopSelectionFunc, | ||
| pub reserved2: *mut c_void, | ||
| pub masked_inner_loop_selector: PyUFunc_MaskedInnerLoopSelectionFunc, | ||
| pub dict: *mut PyObject, |
There was a problem hiding this comment.
This was changed to dict only relatively recently in 2024, see numpy/numpy#27735. It was first available in NumPy 2.2.
| #[cfg(all(Py_3_8, not(Py_LIMITED_API)))] | ||
| pub vectorcall: Option<vectorcallfunc>, | ||
| #[cfg(not(all(Py_3_8, not(Py_LIMITED_API))))] | ||
| pub vectorcall: *mut c_void, |
There was a problem hiding this comment.
This was changed to vectorcall in numpy/numpy#15271 which I believe was first included in NumPy 1.20.0.
| #[cfg(not(all(Py_3_8, not(Py_LIMITED_API))))] | ||
| pub vectorcall: *mut c_void, | ||
|
|
||
| pub reserved3: *mut c_void, |
There was a problem hiding this comment.
Separately from the discussion in this PR, it makes sense to fix these incorrect bindings. Can you fix the issues I pointed out above and send in a PR that doesn't depend on details that changed in later NumPy versions? I think that means wrapping the fields that changed as *mut c_void but maybe @Icxolu has a way they'd prefer to see it spelled.
30dc6e9 to
b1f11de
Compare
|
Thank you for the feedback! I've updated this PR based on @ngoldbaum's comments. The definition of One thought: I believe supporting newer API versions would have a greater beneficial impact than having a support of wider set of versions. Additionally, the code could be significantly simplified if only newer versions of NumPy were supported, like >=1.25, but this choice is out of my responsibility. Sorry @ngoldbaum, but I'm not sure I understand your last comment. Could you clarify what you mean by "details that changed in later NumPy versions"? The definition of |
e.g. following what @Icxolu was saying and not having any conditional compilation or version-dependent bindings. Another thing that I realized after writing my earlier comments is that NumPy itself, as of NumPy 2.0, supports building binaries that work with arbitrary older or newer numpy versions found at runtime. This is tricky and it requires a lot of conditional compilation in NumPy's headers. If it's possible to replicate that and allow setting something analogous to the |
|
@seberg knows more about how numpy handles forward and backward compatibility in its headers and might be interested in this thread too. |
|
Now I have a clearer understanding of what you meant. The mechanism you're referring to is the backward compatibility built into the NumPy API. Each version of NumPy exposes an API that remains compatible with older versions, and users can override the default by defining the This PR aims to replicate that mechanism. By default, the FFI exposed by rust-numpy targets NumPy v1.25, which is the same default used by NumPy v2.0. This may not be the ideal choice, and I'd welcome your input on what would work best. A set of features named Consistency is ensured by the fact that all conditional compilation instructions have been written to mirror the conditionals involving the |
|
I think there's no reason to try to support any numpy version in rust-numpy older than 1.25, so I'd suggest simplifying things here and removing the targets for older versions. It's also not clear to me from the diff that you are handling forward and backward compatibility for different runtime NumPy versions like I think you just claimed - can you explain how you handle that? I'd think you'd need some functions to emulate the macros and static inline functions in the numpy headers. How are you handling the fact that the descriptor object, ufunc object and array object have different layouts in different numpy versions at runtime? |
|
I would think you'd need some sort of automated testing where you build with one numpy version and run with another to validate all this. |
Some of these fields are a bit "why would you ever need them", and I guess those may be sloppy in NumPy (i.e. the dict is probably OK as it was NULL before, but just like vectorcall, you should use the Python APIs...). The thing I think seems missing here is the runtime check (I am sure this already exists for the major C version, but that check needs to be expanded). Validating all fields seems rather tedious, but it seems important to try and validate the runtime version check. (Tedious because most of them just shouldn't be used and many that may be used are still niche to the point that my guess is there is maybe a single external user.) (E.g. a good general candidate is the descr |
We currently support the same Python versions as PyO3 itself. The latest numpy version for 3.7 is 1.21 and for 3.8 is is 1.24.
This is also something that I thought about. Some motivation for APIs that we want to build on top of these would be great. It would maybe that outline a more specific path to a design we could move towards.
This is also unclear to me. One way that I think could maybe work if we want too only add a few APIs that require only a small number of specific fields is to basically define both layouts like so: #[repr(C)]
struct PyArray_Descr {
... // Like currently
}
#[repr(C)]
struct PyArray_DescrV2 {
base: PyArray_Descr,
... // additional V2 fields
}After a runtime version check we could then perform a pointer cast to access the fields or return an error for older numpy versions. But this blows up quite quickly if need a big range of versions and fields, so would be feasible for a small number of combinations. |
|
I spent some time studying the NumPy implementation more closely. This crate is implemented to match the NumPy ABI version 0x02000000, so looking the ABI, it is fully compatible with NumPy v1 and v2. A bit more attention is needed to verify the API compatibility. The chosen version range and the defaults are sufficient to keep the crate compatible with PyO3, as pointed out by @Icxolu. In this PR, I updated the layout of the following three structures:
I believe the changes to structures look good, since all accessor functions to ensure compatibility seem already available. But I don't consider this PR ready to merge yet, since:
I've found this discussion very valuable, thank you all! |
|
I think this PR should be closed without merging. Looking more closely at the code of this crate, I found that it is not ready to support multiple runtime versions in the way I implemented in this PR. Currently, NumPy supports two different versions of the ABI (v1 and v2). Everything compiled targeting v1 cannot work on v2, a RuntimeError is raised on import. Conversely, if something is compiled targeting v2, it could work with v1, provided that the chosen runtime version matches the available NumPy package. These checks are not implemented in this crate in the same way. Instead, a lazy runtime check is implemented to ensure compatibility with both ABIs without compile-time checks. As an extension creator this lack of clarity in the target ABI is a problem. I could call a function available only in NumPy v1 (for example, all the _ARRAY_API slots deprecated in the v2) without any warning, making the extension unusable with NumPy v2. So I would need to take extra care while writing an extension, checking the availability of each function or writing tests that cover all the NumPy API calls. Some other issues related to the mechanism implemented in the crate are:
For me, the priority now is to implement a clear mechanism to target a specific ABI version. By default, the crate should target the latest version of the ABI, but it should be possible to switch to the older one if needed by enabling a feature flag. This would avoid the possibility of calling unavailable functions and accidentally making the extension unusable. I can work on this feature in the coming weeks. But before starting this work, I want to be sure that this is the right approach, so I would like to ask what do you think about this? It will make sense to talk about the choice of runtime only once the target ABI has been clarified. |
Personally, I wouldn't spend much effort to specifically target the NumPy 1.x ABI. I don't think we'd serving anyone by making it easier to use old versions of NumPy. But that's also just my opinion. |
|
First of all, thanks for continuing to looking into this ❤️
I agree, this was my initial feeling as well.
Do we talk about the raw ffi bindings or
This raises the question for me again, why do you need to interact with them, at all. When using the high level API these are not needed and using the raw bindings my question would be why? Could we in this case add to the high level API to make this easier? (223 for example is marked as v1 only and will panic on v2)
I'm asking my self here what is the benefit I get from targeting a specific abi over the current approach (from the perspective of both developing (maintainance) and using the high level api (downstream user)). Maybe you can paint a picture for me here, as I'm still struggling a bit to see what problem we are trying to solve, what the solution should look like and how that solution affect the rest of the api and user experience. I'm open to changes but I would like to understand what we are doing and why we are doing it (mostly this one is missing for my I think). The other thing is that I don't think we can or should use feature flags for this. Cargo features are supposed to be additive and much like the different Python versions for PyO3 itself, this likely requires us using raw
I agree, but I would also make the argument that we should drop v1 support together with PyO3 dropping support for Python 3.7 and 3.8. We could do it sooner I guess, if there is a clear advantage we get that we could not get otherwise. |
This PR introduces the build configuration to select the target Numpy C API version and adjusts struct definitions to match the user selected version.
The default configuration has been chosen to match the setting in the first release of Numpy v2.
The only potentially breaking change is the new definition of
PyUFuncObject. The structPyUFuncObjecthas been updated to match the definition given in Numpy source. The previous definition matched the definition contained in documentation. I think the former is more accurate.I understand that this PR is not small and that not all changes can be easily reviewed, it wasn't easy to put everything together either. If the PR is too large, I could split it into smaller changes. Anyway, I hope it will help improve the FFI.