Skip to content

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

Merged
rgommers merged 5 commits into
numpy:mainfrom
prathamhole14:fft_limited_macro
Jul 16, 2026
Merged

rgommers merged 5 commits into
numpy:mainfrom
prathamhole14:fft_limited_macro

Conversation

@prathamhole14

@prathamhole14 prathamhole14 commented Jul 8, 2026 •

Copy link
Copy Markdown
Member

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 Resolved
Comment thread numpy/fft/meson.build Resolved
@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
Member 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 Resolved
@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
Member 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.

@mattip

mattip commented Jul 15, 2026

Copy link
Copy Markdown
Member

@rgommers could you take another look?

@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.

Okay, this should work - let's roll with this. If it confuses anyone we may need a more extensive code comment, but let's not hold up this PR for it. Thanks @prathamhole14 and reviewers.

@rgommers
rgommers merged commit baa561b into numpy:main Jul 16, 2026
88 checks passed
@prathamhole14
prathamhole14 deleted the fft_limited_macro branch July 16, 2026 12:21
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