You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Relax PyUnstable_SetImmortal to objects allocated in that thread? #150865
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.
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.
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?
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.
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.
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".
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 priorwork 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Activity
ngoldbaum commentedon Jun 3, 2026
cc @kumaraditya303 @colesbury as well
sergey-miryanov commentedon Jun 3, 2026
cc @ZeroIntensity as well too
ZeroIntensity commentedon 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 commentedon 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 commentedon Jun 3, 2026
Sure, I guess
PyUnstable_Object_EnableDeferredRefcountis 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 commentedon Jun 3, 2026
I think type objects use deferred reference counting by default? Maybe that's only for pure-Python types.
seberg commentedon Jun 4, 2026
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 commentedon 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 commentedon 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:
PyUnstable_SetImmortalis 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 removingPyUnstable_SetImmortalwould require a much larger refactor across codebases that use it. (For example, if you were to try and makePy_Nonemortal again in CPython, the diff would be pretty large.)_Py_IsImmortal/PyUnstable_IsImmortalto 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.)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 commentedon 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 commentedon Jun 4, 2026
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.8 remaining items
seberg commentedon Jun 5, 2026
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 commentedon Jun 5, 2026
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 commentedon Jun 5, 2026
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.
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 commentedon 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. 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.freezealready 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 commentedon Jun 17, 2026
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 commentedon Sep 6, 2026
@ZeroIntensity For numpy in numpy/numpy#32513 there are (micro)benchmarks showing the effect on FT scaling.
ZeroIntensity commentedon 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 commentedon 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.
mattip commentedon 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 ana 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
Unstableones) rather than use work-arounds and internal APIs.prathamhole14 commentedon 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.14tsee: numpy/numpy#32552 (comment)Using
subtype_deallocrecovers a lot of this ground (hitting160 M/sat 8 threads vs.230 M/sfor a static type) but it's not a complete fix. The ideal solution is immortal heap types, preferably exposed outsidePyUnstable_so Limited API builds can actually use it.ZeroIntensity commentedon 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, allPy_INCREFcalls 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
_PyStackRefright now), but we're still a few years away from that.