GeezerVison is showing github.com — Relax `PyUnstable_SetImmortal` to objects allocated in that thread? · Issue #150865 · python/cpython
Relax PyUnstable_SetImmortal to objects allocated in that thread? #150865DescriptionFeature 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:
- 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.)
- 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
Activityngoldbaum commented on Jun 3, 2026 sergey-miryanov commented on Jun 3, 2026 ZeroIntensity commented on Jun 3, 2026 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. ngoldbaum commented on Jun 3, 2026 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? seberg commented on Jun 3, 2026 ContributorAuthor More actions 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. ZeroIntensity commented on Jun 3, 2026 I think type objects use deferred reference counting by default? Maybe that's only for pure-Python types. seberg commented on Jun 4, 2026 ContributorAuthor More actions 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. colesbury commented on Jun 4, 2026 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". ZeroIntensity commented on Jun 4, 2026 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:
- 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.
- 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.)
- 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.)
- These objects always leak memory. People will do this anyway via the current API, but relaxing the restrictions will only make this problem worse.
- 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. colesbury commented on Jun 4, 2026 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. seberg commented on Jun 4, 2026 ContributorAuthor More actions 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. seberg commented on Jun 5, 2026 ContributorAuthor More actions 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. seberg commented on Jun 5, 2026 ContributorAuthor More actions 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. ZeroIntensity commented on Jun 5, 2026
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. kumaraditya303 commented on Jun 17, 2026
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. ZeroIntensity commented on Jun 17, 2026
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. eendebakpt commented on Sep 6, 2026
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. ZeroIntensity commented on Sep 6, 2026 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. ZeroIntensity commented on Sep 7, 2026 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. Reacted by Sebastian Berg mattip commented on Sep 25, 2026 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. prathamhole14 commented on Sep 30, 2026 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. ZeroIntensity commented on Sep 30, 2026 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.
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|