Skip to content

ENH: Migrate _multiarray_umath global variables to per-module state - #31992

Merged
kumaraditya303 merged 30 commits into
numpy:mainfrom
prathamhole14:multiarray_module_state
Sep 4, 2026
Merged

kumaraditya303 merged 30 commits into
numpy:mainfrom
prathamhole14:multiarray_module_state

Conversation

@prathamhole14

@prathamhole14 prathamhole14 commented Jul 14, 2026 •

Copy link
Copy Markdown
Member

PR summary

Planner for this migration is this #31930 (comment), but instead of multiple PRs there will be multiple commits in this PR itself.

Related to: #31930

Request to reviewers
I am planning on having 7 major commits in this PR, first commit is for infra, then next 5 for all structs and the 7th is for scattered globals.

So at each stage before I push next commit I would like to get an approval, so I can maintain clean history and this might also turn out useful in future if we want to go back/revert a particular stage or in debugging.

CC: @mattip @ngoldbaum @rgommers

AI Disclosure

Used AI (claude and gemini) for researching and finding out what I have to work on and writing comments (intentionally asked it to make comments verbose for now -- so they can be used as trackers and later I will make them concise) other than that most of the code, logic, and architecture were written by me.

@mattip

mattip commented Jul 14, 2026

Copy link
Copy Markdown
Member

This looks correct to me so far. Do we have module teardown tests to make sure the GC paths are hit? You can try adding a temporary debug print statement in the m_traverse, m_clear, m_free functions and then run tests to make sure they are called somewhere.

@prathamhole14

Copy link
Copy Markdown
Member Author

You can try adding a temporary debug print statement in the m_traverse, m_clear, m_free functions and then run tests to make sure they are called somewhere.

I Added temporary fprintf(stderr, ...) to all three GC hooks and ran gc.collect() test (and later used spin test as well) after importing NumPy. The output confirmed m_traverse is called repeatedly (as expected) but did not get an m_clear or m_free. Is this expected as they happen only after interpreter/subinterpreter shutdown?

sys.stderr.flush()
n = gc.collect()
sys.stderr.flush()

Also I am ready with the next commit locally, please let me know when can I push it!

@mattip

mattip commented Jul 15, 2026

Copy link
Copy Markdown
Member

Is the global state the only blocker for subinterpreter support or are there more nuances I am not aware of? Maybe we could add a xfailing test that adds a subinterpreter and shuts it down, I don't think it should segfault. Then in that test you could make sure it hits the code paths but still fails since we do not yet support subinterpreters.

Go ahead with the next commit as well.

@prathamhole14

prathamhole14 commented Jul 17, 2026 •

Copy link
Copy Markdown
Member Author

If someone could please "approve" Migrate npy_static_cdata to module state, then I will push the next migration commit afterwards thanks.

Edit: Oops I had to push Migrate npy_global_state to per-module state since I did some minor ref in Migrate npy_static_cdata to module state

@prathamhole14
prathamhole14 force-pushed the multiarray_module_state branch from e44a7cf to 131f8e9 Compare July 17, 2026 09:23
@ngoldbaum

Copy link
Copy Markdown
Member

Does this have performance overhead? I looked over the diff and didn’t see any spots where we call the function in a tight loop so I doubt there is, but I want to make sure you’re thinking about that.

@prathamhole14

This comment was marked as outdated.

@ngoldbaum

Copy link
Copy Markdown
Member

Why did you choose that benchmark to look at? Would you expect the changes in this PR to have a significant impact? If so, why? It’s not clear to me and I suspect you’re measuring noise.

@prathamhole14

Copy link
Copy Markdown
Member Author

I first ran spin bench -c main -t bench_core on the multiarray_module_state branch (results above) since I was not sure how much can I trust the benchmarks, that's why intentionally I chose bench_core.CountNonzero to find out how much is the noise because it showed the largest regressions (1.23x) and I also think my changes from this PR shouldn't affect CountNonzero.

Would you expect the changes in this PR to have a significant impact?

Nope, I think it should be little to none till now atleast (this is my intuition - needs reliable testing)

If so, why? It’s not clear to me and I suspect you’re measuring noise.

Yes I was measuring noise. Here are the bench_core.CountNonzero benchmarks against dummy commit I did earlier:
Even with completely identical code, the results fluctuated by ±10% on my pc.

| Change   | Before [941bc425] <main>   | After [c3e0325a] <test_benchmark_noise>   |   Ratio | Benchmark (Parameter)                                                             |
|----------|----------------------------|-------------------------------------------|---------|-----------------------------------------------------------------------------------|
| +        | 601±20μs                   | 664±30μs                                  |    1.1  | bench_core.CountNonzero.time_count_nonzero(3, 1000000, <class 'numpy.int32'>)     |
| +        | 57.6±0.3μs                 | 62.5±0.1μs                                |    1.08 | bench_core.CountNonzero.time_count_nonzero_multi_axis(3, 10000, <class 'str'>)    |
| +        | 6.00±0.09μs                | 6.39±0.3μs                                |    1.07 | bench_core.CountNonzero.time_count_nonzero_multi_axis(2, 100, <class 'object'>)   |
| -        | 56.0±2μs                   | 50.6±0.2μs                                |    0.9  | bench_core.CountNonzero.time_count_nonzero(3, 10000, <class 'str'>)               |
| -        | 305±10μs                   | 274±2μs                                   |    0.9  | bench_core.CountNonzero.time_count_nonzero_multi_axis(2, 10000, <class 'object'>) |

SOME BENCHMARKS HAVE CHANGED SIGNIFICANTLY.
PERFORMANCE DECREASED.

@prathamhole14
prathamhole14 force-pushed the multiarray_module_state branch 3 times, most recently from 36cd184 to 5fffd0c Compare July 21, 2026 18:10
@prathamhole14

This comment was marked as outdated.

@prathamhole14
prathamhole14 force-pushed the multiarray_module_state branch 2 times, most recently from 00bb6af to 7f05969 Compare July 22, 2026 06:15
@prathamhole14
prathamhole14 force-pushed the multiarray_module_state branch from 473d087 to 5a7399a Compare July 25, 2026 08:22
@prathamhole14

Copy link
Copy Markdown
Member Author

Dug into the remaining scattered globals (PyDataMem_DefaultHandler,userdescrs/NPY_NUMUSERTYPES, and the DType singleton pointers in dtypemeta.c/abstractdtypes.c). These cannot be migrated right now since all of them are entries in PyArray_API, one literal array compiled into the .so, so there's exactly one copy per process. A module state field only fixes our internal copy, external code still reads the old shared slot directly.

Two categories:

  1. PyDataMem_DefaultHandler and NPY_NUMUSERTYPES are raw variable slots, external code dereferences PyArray_API[n] as the variable's own memory. Making them real per interpreter means turning them into accessor functions instead, same pattern as PyDataMem_SetHandler and GetHandler already use. That's an ABI break per numpy_api.py's own rules and needs a C API version change.

  2. The DType pointers store the address of one canonical type object. Heap typing them fixes a different problem, one struct being mutated by multiple interpreters, not the table only being able to hold one address per slot. Same category as PyArray_Type.

Questions for you all:

  • PyDataMem_DefaultHandler is basically read only after init. Is it fine to document as a permanent shared exception or do we want the ABI change?
  • NPY_NUMUSERTYPES can't just be documented as shared, PyArray_RegisterDataType still mutates it live, so migrating userdescrs alone would desync registrations across interpreters. ABI fix, or do we document legacy user dtype registration as not subinterpreter safe?
  • DType singletons, is shared type identity across interpreters acceptable? or do we want per interpreter type tables eventually?

None of this blocks what this PR already have, it only blocks the final Py_mod_multiple_interpreters flip and the TestSubinterpreterTeardown xfail. I am not sure if this is the right place to discuss this, maybe a separate issue?

Comment thread numpy/_core/src/umath/ufunc_object.c Resolved
@mattip mattip added the triage review Issue/PR to be discussed at the next triage meeting label Aug 31, 2026

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

Just silly comments browsing through, don't worry about them.

I suppose until there is actually more than one global state this effectively does nothing (e.g. we can't remove the guards against reloading NumPy -- although I guess the full thing there may be harder and also require dealing with heap types, etc... seems like a very long tail to get somewhere).

(I am slightly wondering if we should just almost always use an early function level state = ... even if ... is _npy_module_state for now. But it also doesn't matter here, more of a mid-term code style question.)

*
* FIXME: Remove once all access sites receive the module or state pointer.
*/
NPY_VISIBILITY_HIDDEN extern multiarray_umath_state *_npy_module_state;

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.

(pretty unrelated to merging) So I guess we mentioned it yesterday a bit, but what is the plan here, how much of this can we fix up in a relatively quick followup and how much of it is blocked/hard to do?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think this will take a lot of time, I can create 5 parallel PRs (or a single big one) for propagation and after this the FIXME can be addressed. But I am thinking if I should do limited api work first or the one I just mentioned since per module state is not requirement for LIMITED API (it is for subinterpreters).

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.

I would like to see progress on limited API. Can we open a follow-on issue for the remaining fixup?

extern "C" {
#endif

#define NPY_INTERNED_STR_FIELDS(F) \

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.

Likely it's just the way to do it, but my first reaction to seeing this is "there should be way to avoid duplicating all fields".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Let's address this later and merge this PR since so I can work on LIMITED API asynchronously :)

@kumaraditya303

Copy link
Copy Markdown
Contributor

I'll merge this so that @prathamhole14 can work on further limited API changes, the remaining fixups can be done in follow up PRs. Thanks all for reviews.

@kumaraditya303
kumaraditya303 merged commit 8fecb3b into numpy:main Sep 4, 2026
90 of 91 checks passed
@prathamhole14
prathamhole14 deleted the multiarray_module_state branch September 4, 2026 14:05
@prathamhole14 prathamhole14 added triaged Issue/PR that was discussed in a triage meeting and removed triage review Issue/PR to be discussed at the next triage meeting labels Sep 4, 2026
eendebakpt added a commit to eendebakpt/numpy that referenced this pull request Oct 23, 2026
The global ``npy_static_pydata`` struct was migrated to per-module state
in numpygh-31992, which broke the build after merging main.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

01 - Enhancement triaged Issue/PR that was discussed in a triage meeting

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants