Skip to content

ENH: Add per-extension Limited API toggle and port _pocketfft_umath to be limited api compatible#31910

Open
prathamhole14 wants to merge 5 commits into
numpy:mainfrom
prathamhole14:fft_limited_macro
Open

ENH: Add per-extension Limited API toggle and port _pocketfft_umath to be limited api compatible#31910
prathamhole14 wants to merge 5 commits into
numpy:mainfrom
prathamhole14:fft_limited_macro

Conversation

@prathamhole14

@prathamhole14 prathamhole14 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

PR summary

Background: Since _pocketfft_umath is smaller I decided to tackle it first but realized there is no need for refractor to structs there.
Therefore I decided to add #define Py_LIMITED_API 0x030c0000 in the cpp file and compile which worked fine on my machine (linux-ubuntu) after a few tweaks, but failed on CI -- instead of the preprocessor, limited_api is to be configured in meson.build therefore I followed the pattern from the below (since checks on CI failed with #error "The limited API is not currently supported in the free-threaded build" .

if py.get_variable('Py_GIL_DISABLED', 0) == 0

Related to: #31913

CC: @mattip

Edit: To create abi3.so compiled files (could be for testing only) i.e. modules with limited api, use -Dpython.allow_limited_api=true flag while installing or build numpy.

AI Disclosure

None

@prathamhole14 prathamhole14 marked this pull request as ready for review July 8, 2026 10:39

@rgommers rgommers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for starting to look into the limited API topic @prathamhole14, looking forward to seeing how far that can be pushed.

This PR isn't mergeable I'm afraid. We should not actually convert individual extensions to already compile to abi3.so extensions. That makes the meson.build files more complex, potentially causes some slowdowns (not in this case, but in general), and has no upsides.

If C/C++-level changes are needed, I think those we can make. The two changes here seem unnecessary though. When you find that source-level changes are needed, that is usually to avoid APIs that aren't part of the Limited C API. At that point the most interesting question (after it builds) is what the performance cost of the changes is, if any - so benchmarks are usually good to include.

Comment thread numpy/fft/_pocketfft_umath.cpp Outdated
static struct PyModuleDef_Slot _pocketfft_umath_slots[] = {
{Py_mod_exec, (void*)_pocketfft_umath_exec},
#if PY_VERSION_HEX >= 0x030c00f0 // Python 3.12+
#if PY_VERSION_HEX >= 0x030c00f0 && (!defined(Py_LIMITED_API) || Py_LIMITED_API >= 0x030c0000) // Python 3.12+

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Both changes to this file look unnecessary - it should compile fine without this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OH yes the first one is unnecessary as min required version is 3.12

But I think the second one does require the check, because if we set Py_LIMITED_API=3.12, the 3.13 slot gets hidden, and it will fail to compile on Python 3.13 without that check?

I tested it on 3.13, I get the following error.

  ../../numpy/fft/_pocketfft_umath.cpp:427:6: error: 'Py_mod_gil' was not declared in this scope
    427 |     {Py_mod_gil, Py_MOD_GIL_NOT_USED},
        |      ^~~~~~~~~~
  ../../numpy/fft/_pocketfft_umath.cpp:427:18: error: 'Py_MOD_GIL_NOT_USED' was not declared in this scope
    427 |     {Py_mod_gil, Py_MOD_GIL_NOT_USED},
        |                  ^~~~~~~~~~~~~~~~~~~

Comment thread numpy/fft/meson.build Outdated
@rgommers

rgommers commented Jul 9, 2026

Copy link
Copy Markdown
Member

It may be useful to include a tracker in gh-31913 per extension module. This one can be marked off as working in principle I'd say.

@rgommers

rgommers commented Jul 9, 2026

Copy link
Copy Markdown
Member

If it gets too annoying to work on this without being able to toggle per-extension-module support, we can add build system support along the lines of the PyWavelets/pywt#828. It will be opt-in and a private build option flag, so it's hidden except for the purposes of working on this topic. Then the only change per extension module should be to add one line like:

    limited_api: py_limited_api,

with py_limited_api define once centrally.

@prathamhole14

prathamhole14 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks you @rgommers for the detailed reviews.

If it gets too annoying to work on this without being able to toggle per-extension-module support, we can add build system support along the lines of the PyWavelets/pywt#828. It will be opt-in and a private build option flag, so it's hidden except for the purposes of working on this topic. Then the only change per extension module should be to add one line like:

    limited_api: py_limited_api,

with py_limited_api define once centrally.

Yes it makes more sense to do this, just pushed the code in the last commit for this. For now I have set the default to false so we won't be shipping abi3.so files, but yes we for testing locally we can use this flag -Csetup-args=-Dpython.allow_limited_api=true. I will post benchmarks soon.

Comment thread meson.build Outdated
@prathamhole14 prathamhole14 changed the title ENH: Make _pocketfft_umath compile with Limited API ENH: Add per-extension Limited API toggle and port _pocketfft_umath to be limited api compatible Jul 10, 2026
@prathamhole14

prathamhole14 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author
| Change   | Before [dd4339c4] <main>   | After [a0ef3688] <fft_limited_macro>   |   Ratio | Benchmark (Parameter)                                                                      |
|----------|----------------------------|----------------------------------------|---------|--------------------------------------------------------------------------------------------|
| +        | 9.00±0.2μs                 | 10.9±0.7μs                             |    1.21 | bench_core.StatsMethods.time_prod('float32', 10000)                                        |
| +        | 389±5μs                    | 447±20μs                               |    1.15 | bench_core.CountNonzero.time_count_nonzero_axis(1, 1000000, <class 'bool'>)                |
| +        | 15.9±0.3μs                 | 18.1±1μs                               |    1.14 | bench_core.CountNonzero.time_count_nonzero_multi_axis(3, 10000, <class 'bool'>)            |
| +        | 17.7±0.3μs                 | 20.1±0.9μs                             |    1.14 | bench_core.CountNonzero.time_count_nonzero_multi_axis(3, 10000, <class 'numpy.int8'>)      |
| +        | 9.95±0.07μs                | 11.3±0.4μs                             |    1.14 | bench_core.NumPyChar.time_isalpha_big_list_small_string                                    |
| +        | 1.18±0.01ms                | 1.32±0.05ms                            |    1.12 | bench_core.CountNonzero.time_count_nonzero_multi_axis(3, 1000000, <class 'bool'>)          |
| +        | 3.01±0.08ms                | 3.31±0.09ms                            |    1.1  | bench_core.CorrConv.time_correlate(100000, 100, 'full')                                    |
| +        | 10.4±0.2μs                 | 11.5±0.3μs                             |    1.1  | bench_core.CountNonzero.time_count_nonzero_axis(1, 10000, <class 'numpy.int32'>)           |
| +        | 157±1μs                    | 173±7μs                                |    1.1  | bench_core.CountNonzero.time_count_nonzero_multi_axis(1, 10000, <class 'object'>)          |
| +        | 11.3±0.2μs                 | 12.5±0.3μs                             |    1.1  | bench_core.CountNonzero.time_count_nonzero_multi_axis(2, 10000, <class 'bool'>)            |
| +        | 4.30±0.07μs                | 4.67±0.2μs                             |    1.09 | bench_core.CountNonzero.time_count_nonzero_axis(2, 100, <class 'numpy.int16'>)             |
| +        | 784±9μs                    | 854±10μs                               |    1.09 | bench_core.CountNonzero.time_count_nonzero_multi_axis(2, 1000000, <class 'bool'>)          |
| +        | 19.2±0.7μs                 | 21.0±0.4μs                             |    1.09 | bench_core.CountNonzero.time_count_nonzero_multi_axis(3, 10000, <class 'numpy.int16'>)     |
| +        | 320±5μs                    | 347±20μs                               |    1.08 | bench_core.CountNonzero.time_count_nonzero_multi_axis(2, 10000, <class 'object'>)          |
| +        | 25.3±0.5μs                 | 27.2±1μs                               |    1.08 | bench_core.CountNonzero.time_count_nonzero_multi_axis(3, 10000, <class 'numpy.int64'>)     |
| +        | 1.24±0.01μs                | 1.34±0.06μs                            |    1.08 | bench_core.Nonzero.time_nonzero_sparse(<class 'numpy.float32'>, (100,))                    |
| +        | 3.33±0.1μs                 | 3.58±0.1μs                             |    1.08 | bench_core.NumPyChar.time_find_small_list_big_string                                       |
| +        | 305±5μs                    | 325±20μs                               |    1.07 | bench_core.CorrConv.time_correlate(100000, 10, 'same')                                     |
| +        | 10.5±0.4μs                 | 11.3±0.3μs                             |    1.07 | bench_core.CountNonzero.time_count_nonzero_multi_axis(1, 10000, <class 'numpy.float32'>)   |
| +        | 4.86±0.08μs                | 5.19±0.2μs                             |    1.07 | bench_core.CountNonzero.time_count_nonzero_multi_axis(3, 100, <class 'numpy.float64'>)     |
| +        | 972±10ns                   | 1.04±0.03μs                            |    1.07 | bench_core.StatsMethods.time_sum('int64', 100)                                             |
| +        | 11.2±0.2μs                 | 11.8±0.2μs                             |    1.06 | bench_core.CountNonzero.time_count_nonzero_axis(2, 10000, <class 'bool'>)                  |
| -        | 2.25±0.01ms                | 2.13±0.03ms                            |    0.95 | bench_core.CountNonzero.time_count_nonzero_axis(2, 1000000, <class 'numpy.int64'>)         |
| -        | 4.72±0.1μs                 | 4.44±0.08μs                            |    0.94 | bench_core.CountNonzero.time_count_nonzero_multi_axis(1, 100, <class 'numpy.int8'>)        |
| -        | 4.17±0.07μs                | 3.90±0.04μs                            |    0.94 | bench_core.CountNonzero.time_count_nonzero_multi_axis(3, 100, <class 'bool'>)              |
| -        | 1.23±0.03μs                | 1.14±0μs                               |    0.93 | bench_core.Nonzero.time_nonzero(<class 'numpy.uint8'>, (100,))                             |
| -        | 930±9ns                    | 865±20ns                               |    0.93 | bench_core.Nonzero.time_nonzero_sparse(<class 'bool'>, (100,))                             |
| -        | 5.80±0.1μs                 | 5.38±0.1μs                             |    0.93 | bench_core.StatsMethods.time_mean('float32', 100)                                          |
| -        | 621±6μs                    | 573±9μs                                |    0.92 | bench_core.CountNonzero.time_count_nonzero(2, 1000000, <class 'numpy.int32'>)              |
| -        | 11.2±0.3μs                 | 10.4±0.3μs                             |    0.92 | bench_core.StatsMethods.time_mean('int64', 10000)                                          |
| -        | 23.3±2μs                   | 21.5±0.3μs                             |    0.92 | bench_core.StatsMethods.time_var('float32', 10000)                                         |
| -        | 710±30μs                   | 645±5μs                                |    0.91 | bench_core.CountNonzero.time_count_nonzero_axis(1, 1000000, <class 'numpy.float32'>)       |
| -        | 14.4±0.5μs                 | 13.0±0.2μs                             |    0.91 | bench_core.CountNonzero.time_count_nonzero_axis(2, 10000, <class 'numpy.int8'>)            |
| -        | 2.52±0.08μs                | 2.29±0.01μs                            |    0.91 | bench_core.StatsMethods.time_max('float64', 10000)                                         |
| -        | 456±9μs                    | 409±10μs                               |    0.9  | bench_core.CountNonzero.time_count_nonzero_axis(1, 1000000, <class 'numpy.int8'>)          |
| -        | 1.26±0.03ms                | 1.13±0.03ms                            |    0.9  | bench_core.CountNonzero.time_count_nonzero_axis(2, 1000000, <class 'numpy.int16'>)         |
| -        | 1.68±0.03ms                | 1.52±0.05ms                            |    0.9  | bench_core.CountNonzero.time_count_nonzero_axis(2, 1000000, <class 'numpy.int32'>)         |
| -        | 4.94±0.3μs                 | 4.45±0.03μs                            |    0.9  | bench_core.CountNonzero.time_count_nonzero_multi_axis(2, 100, <class 'numpy.int16'>)       |
| -        | 600±60ns                   | 525±7ns                                |    0.88 | bench_core.CountNonzero.time_count_nonzero(1, 100, <class 'numpy.int32'>)                  |
| -        | 614±20ns                   | 540±7ns                                |    0.88 | bench_core.CountNonzero.time_count_nonzero(1, 100, <class 'numpy.int64'>)                  |
| -        | 765±30μs                   | 675±20μs                               |    0.88 | bench_core.CountNonzero.time_count_nonzero_multi_axis(1, 1000000, <class 'numpy.float32'>) |
| -        | 2.13±0.1μs                 | 1.87±0.02μs                            |    0.88 | bench_core.StatsMethods.time_sum('bool_', 100)                                             |
| -        | 18.1±2μs                   | 15.8±0.4μs                             |    0.87 | bench_core.CountNonzero.time_count_nonzero_multi_axis(2, 10000, <class 'numpy.float32'>)   |
| -        | 4.44±0.3μs                 | 3.86±0.04μs                            |    0.87 | bench_core.StatsMethods.time_mean('bool_', 100)                                            |
| -        | 101±4μs                    | 86.5±2μs                               |    0.85 | bench_core.CountNonzero.time_count_nonzero_axis(3, 10000, <class 'str'>)                   |
| -        | 13.2±1μs                   | 11.2±0.2μs                             |    0.85 | bench_core.StatsMethods.time_mean('uint64', 10000)                                         |
| -        | 3.40±0.2μs                 | 2.60±0.1μs                             |    0.76 | bench_core.StatsMethods.time_min('uint64', 10000)                                          |
| -        | 593±200ns                  | 338±20ns                               |    0.57 | bench_core.Core.time_array_1                                                               |
| -        | 95.6±7μs                   | 50.7±30μs                              |    0.53 | bench_core.CorrConv.time_convolve(50, 1000, 'same')                                        |
| -        | 92.0±4μs                   | 48.9±20μs                              |    0.53 | bench_core.CorrConv.time_convolve(50, 1000, 'valid')                                       |
| -        | 1.77±0.3μs                 | 830±300ns                              |    0.47 | bench_core.Core.time_arange_100                                                            |
| -        | 1.14±0.2μs                 | 508±60ns                               |    0.44 | bench_core.Core.time_array_empty                                                           |
| -        | 129±20μs                   | 50.5±4μs                               |    0.39 | bench_core.Core.time_array_float64_l1000                                                   |
| -        | 122±20μs                   | 45.8±2μs                               |    0.38 | bench_core.Core.time_array_float_l1000                                                     |
| -        | 118±10μs                   | 40.6±2μs                               |    0.34 | bench_core.Core.time_array_float_l1000_dtype                                               |

SOME BENCHMARKS HAVE CHANGED SIGNIFICANTLY.
PERFORMANCE DECREASED.

I ran the benchmarks (spin bench -c main -t bench_core) after installing _pocketfft_umath's abi3.so and noticed a decrease in performance, is this expected or something wrong with benchmarks on my machine?

@mattip

mattip commented Jul 10, 2026

Copy link
Copy Markdown
Member

Yes, if you do not carefully set up your machine for benchmarking, benchmarks with microsecond timings can vary 10-20%. Note your changes only touch fft, so this run is a good way to show the variability of your system. Laptops with no fans are particularly vulnerable to heat-related clock governing changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants