Skip to content

Relax PyUnstable_SetImmortal to objects allocated in that thread? #150865

Open

Description

@seberg

Feature or enhancement

Proposal:

Currently PyUnstable_SetImmortal requires a uniquely referenced object. I think the docs could be a bit clearer about this but that isn't the tricky part.

The tricky part is that I think it doesn't seem uncommon to create objects which are not uniquely referenced. I have two overlapping places in NumPy where I was hope to use this while making types that were previously static to heaptypes:

  1. If you have non-trivial setup code that might fail you want to immortalize at the very end. In my case I wanted to add the object to a dict (as a last step). (Sure, if I am going to immortalize it, then I can re-order adding it to the dict with a lock or so.)
  2. I am using PyType_FromMetaType() and the returned object already has 3 references when created fresh.

Basically, I get the "err on the safe side" part, but from poking it a bit and looking at #113956 it seems that the real limitation seems to be that the current thread owns the object.

Ensuring the object is created on the current thread is trivial no matter how complicated object creation itself is.

CC @ngoldbaum

Has this already been discussed elsewhere?

No response given

Links to previous discussion of this feature:

No response

Linked PRs

Activity

  1. ngoldbaum commented on Jun 3, 2026

    @ngoldbaum
    Contributor
  2. sergey-miryanov commented on Jun 3, 2026

    @sergey-miryanov
    Contributor

    cc @ZeroIntensity as well too

  3. ZeroIntensity commented on Jun 3, 2026

    @ZeroIntensity
    Member

    It is extremely unsafe to immortalize an object that isn't uniquely referenced and immutable (which heap types are not). I don't think this is something that should be in the public API -- if you really want to do it, use the private API.

  4. ngoldbaum commented on Jun 3, 2026

    @ngoldbaum
    Contributor

    Perhaps off-base suggestion: maybe what you really want is to enable deferred reference counting? Or are you worried about something besides scaling on the free-threaded build?

  5. seberg commented on Jun 3, 2026

    @seberg
    ContributorAuthor

    Sure, I guess PyUnstable_Object_EnableDeferredRefcount is probably enough then. In the end I just care to about being able to say: "nah, there is no reason to expect a perf regression" in case someone asks (because someone always do no matter if all agree that heap-types are nice to use).

    These are types may be incref'd/decref'd a fair bit and at least and they are effectively immortal (and at least for own ones also immutable, since the corresponding flag is set). So immutability just seemed obvious.

  6. ZeroIntensity commented on Jun 3, 2026

    @ZeroIntensity
    Member

    I think type objects use deferred reference counting by default? Maybe that's only for pure-Python types.

  7. seberg commented on Jun 4, 2026

    @seberg
    ContributorAuthor

    OK, I guess I'll close this. Thanks for the info! (I am not following why this is unsafe, but these type of things tend to be complicated either way...)
    Maybe it could make sense to just immortalize the hard way when it's clear that it is safe, but I'll just guess the performance difference is very small in practice.

  8. colesbury commented on Jun 4, 2026

    @colesbury
    Contributor

    I'm a bit late to this, but this seems fine to me. I don't see why it's "extremely unsafe to immortalize an object that isn't uniquely referenced and immutable".

  9. ZeroIntensity commented on Jun 4, 2026

    @ZeroIntensity
    Member

    Sorry, "extremely unsafe" might have been misleading on my part. I meant that it's very difficult to get this right, and I think there are a lot of edge cases here that will be hard to reason about and hard to debug. It's definitely possible to get it right, but I don't believe it's something we want in the public API.

    For context, I've done a lot of prior work trying to immortalize mutable objects. It's been a while since I last worked on it, but off the top of my head, some of the issues I encountered were:

    1. Many objects make assumptions about reference count differences. If you know that all objects referencing the immortalized object aren't doing this, then it's fine, but there's no good way to validate this.
    2. A lot of the benefits of immortality (such as avoiding reference count contention) are tied to the current implementation. This is fine because PyUnstable_SetImmortal is unstable, but people can indirectly rely on it through public, stable APIs. When I discussed this with others, their main concern was that people omit reference counting operations for immortal objects, so removing PyUnstable_SetImmortal would require a much larger refactor across codebases that use it. (For example, if you were to try and make Py_None mortal again in CPython, the diff would be pretty large.)
    3. Immortality is generally supposed to imply immutability. Some things use _Py_IsImmortal/PyUnstable_IsImmortal to detect if something is immutable, not just immortal. (I think there are some subinterpreter things that do this as an example, but I can't remember exactly where.)
    4. These objects always leak memory. People will do this anyway via the current API, but relaxing the restrictions will only make this problem worse.
    5. In general, plenty of objects in CPython are special and will break in weird ways when made immortal. If you look at my implementation, you'll find a bunch of changes I had to make across the codebase to support immortalization.

    In NumPy's case, these might not come up, but by relaxing this restriction, we're opening the door to all of these problems. In other words, it's certainly possible to support this, but the contract would have to be more complicated than just "uniquely referenced by the calling thread", which is why I think it should stay in the private API for now -- we don't want to shift those complications onto ourselves.

  10. colesbury commented on Jun 4, 2026

    @colesbury
    Contributor

    The mixing up of immutability and immortality has always been wrong. Immortality does not imply immutability and arbitrary objects can be made immortal if their reference count field overflows.

  11. seberg commented on Jun 4, 2026

    @seberg
    ContributorAuthor

    Yeah, I guess that PEP draft wants to share between subinterpreters so requires immutability (very hard), while immortalization just means we might effectively immortalize things kept alive?

    Either way, I am happy if it makes sense and I'll re-open (if just because to me it feels pretty natural to want to immortalize objects more generally).
    But if performance wise immortalization is almost no different to deferred (besides maybe for something as heavily used as None, etc.), then I also don't really care.

  12. 8 remaining items

  13. seberg commented on Jun 5, 2026

    @seberg
    ContributorAuthor

    This came out of numpy/numpy#31364 where I want to make all DTypes HeapTypes (since it seems to me that is generally a good transition to do). And it just felt naturally to want to immortalize them (because they are effectively immortal); even if I can't say I know that refcounting contention will be an issue in practice.

    And of course I imagined that wanting to immortalize a somewhat less trivial object where ensuring a refcount of 1 might be hard may occur in other places.

    To me the main question was whether it is fully safe if the thread owns it (even if I believe it means that the deferred refcount part in a free-threaded build may end up with a non-zero value). And I guess the overflow case runs into the same thing so that it must be.

  14. seberg commented on Jun 5, 2026

    @seberg
    ContributorAuthor

    Thought I'd open a PR just in case. That said, I am myself not really clear about possible caveats of immortalizing on either free-threaded or not (since I think the current function does both).
    Which I guess might mean we should just close it, add some more scary docs, or just be fine.

  15. ZeroIntensity commented on Jun 5, 2026

    @ZeroIntensity
    Member

    And it just felt naturally to want to immortalize them (because they are effectively immortal); even if I can't say I know that refcounting contention will be an issue in practice.

    This is what worries me -- it should very rarely feel "natural" to immortalize something, especially GC-tracked objects on the heap. Looking at that PR, I think it makes more sense to use DRC than to immortalize the type.

    And I guess the overflow case runs into the same thing so that it must be.

    I'm unconvinced that the overflow thing implies that this already works. More likely, accidental immortalization leads to a bunch of existing bugs that haven't been noticed yet, simply because nobody has 2^32 references in practice.

  16. kumaraditya303 commented on Jun 17, 2026

    @kumaraditya303
    Contributor

    I think it makes more sense to use DRC than to immortalize the type.

    Deferred reference counting only helps in reducing contention from the interpreter's stack, numpy on the other hand mostly involves C-API calls where regulars increfs are used. The only way to avoid contention from these is to make the objects immortal.

    I don't quite understand why immortalizing GC objects is a bad idea or the memory leak caused by it is problematic. gc.freeze already allows gc objects to be made effectively uncollectable and that does leak memory.

    I think we should relax the constraint to "object should be owned by the current thread", other than that I don't think there should be any other checks.

  17. ZeroIntensity commented on Jun 17, 2026

    @ZeroIntensity
    Member

    Deferred reference counting only helps in reducing contention from the interpreter's stack, numpy on the other hand mostly involves C-API calls where regulars increfs are used.

    Yes, but it's not clear to me as to whether this is actually causing a scaling bottleneck here. If there were concrete benchmarks showing that the only way to achieve good performance is to immortalize the type in question, then I'd be more receptive.

    Did you read my comments on the PR? Regardless of whether we do this or not, the constraints seem more complicated than just "owned by the current thread", at least with the proposed implementation.

  18. eendebakpt commented on Sep 6, 2026

    @eendebakpt
    Contributor

    Deferred reference counting only helps in reducing contention from the interpreter's stack, numpy on the other hand mostly involves C-API calls where regulars increfs are used.

    Yes, but it's not clear to me as to whether this is actually causing a scaling bottleneck here. If there were concrete benchmarks showing that the only way to achieve good performance is to immortalize the type in question, then I'd be more receptive.

    @ZeroIntensity For numpy in numpy/numpy#32513 there are (micro)benchmarks showing the effect on FT scaling.

  19. ZeroIntensity commented on Sep 6, 2026

    @ZeroIntensity
    Member

    I'm not opposed to relaxing the API; I'm just making the case that the requirements are much more complicated than "owned by the current thread", because the interpreter has various assumptions about where things can and cannot be immortal.

  20. ZeroIntensity commented on Sep 7, 2026

    @ZeroIntensity
    Member

    I'm going to take a stab at this using my old implementation as a reference. It won't be nearly as complicated because we won't need to deallocate the immortalized object during finalization.

  21. self-assigned this
    on Sep 7, 2026
  22. mattip commented on Sep 25, 2026

    @mattip
    Contributor

    The inability to mark objects immortal has caused NumPy to use borrowed references for performance reasons. We would have been happier to use immortality to avoid borrowed references.

    This PR was forced to use CPython an a CPython internal API to mark objects immortal, again for performance reasons. In fact the PR mentions this discussion.

    We would be happier to use "blessed" APIs (preferably not Unstable ones) rather than use work-arounds and internal APIs.

  23. prathamhole14 commented on Sep 30, 2026

    @prathamhole14

    Another data point: migrating NumPy types to heap types (for the Limited API and subinterpreters) crushes free-threaded scaling. @ngoldbaum benchmarked neighborhood iterators on 3.14t see: numpy/numpy#32552 (comment)

    Using subtype_dealloc recovers a lot of this ground (hitting 160 M/s at 8 threads vs. 230 M/s for a static type) but it's not a complete fix. The ideal solution is immortal heap types, preferably exposed outside PyUnstable_ so Limited API builds can actually use it.

  24. ZeroIntensity commented on Sep 30, 2026

    @ZeroIntensity
    Member

    Yes, sorry, I've been meaning to get around to this. I think there's a very low chance that we put this outside of PyUnstable; immortality is closely tied to CPython's reference counting implementation, and we don't want to commit to that for eternity. Limited API builds already have some performance problems (for example, all Py_INCREF calls cannot be inlined). The solution is to ship both per-version wheels and a limited API wheel for the distant future where NumPy isn't maintained anymore.

    Long term, I'm still not super comfortable with people immortalizing everything that is touched by C code. The better solution would be to migrate to a new HPy-like C API layer that can encode deferred reference counting in its object type (basically what we do with _PyStackRef right now), but we're still a few years away from that.

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

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions