I think this is generally unsafe, not just for free threading. The vectorcall args array needs to be valid for the duration of the call, and it's possible for methodcaller to be called reentrantly or by another thread while the call is still ongoing.
changed the title [-]Free threading race condition involving descriptor lookup[/-][+]`methodcaller` is not thread-safe (or re-entrant)[/+]on Nov 20, 2024
I think the optimized vectorcall implementation from #107201 is not reentrant or thread-safe (even with the GIL) because the shared vectorcall_args array is modified during each invocation:
Most (or all) of our current vectorcall implementations extract out args[0] early, but that's not guaranteed and the vectorcall protocol is a public API.
For now, I'm planning to just disable the optimization in the free threading build (#127109), but I don't see an easy way of making it thread-safe, and I think we should consider reverting #107201 in 3.13 and main.
I agree this looks like a bug also in the normal build. The issue is hard to trigger in the normal build though, as indeed most vectorcall implementations (including the PyObject_VectorcallMethod used by the methodcaller) get args[0] before doing any work).
One option to make the vectorcall implementation thread-safe (and re-entrant) would be to copy the entire mc->vectorcall_args to a temporary buffer before setting the first element to args[0]. An implementation of this idea is main...eendebakpt:methodcaller_ft.
A quick benchmark (using the script from #107201) shows there is still a significant performance increase:
I removed the draft status from the PR (thanks to @colesbury for some good suggestions). Both PR #127109 or reverting #107201 are good options I think (a classic trade-off between performance and added complexity, except that in this case the status quo is not good)
The issue has been addressed on main and on 3.13 the optimization has been removed for the free-threading build. I think we can close this issue, unless we would also like to address the issue for 3.13 with the normal build.
Activity
colesbury commented on Nov 20, 2024
Hmm, actually it looks like a thread safety issue in methodcaller.
ngoldbaum commented on Nov 20, 2024
Yeah, I noticed early it wasn't triggering if I called the descriptors directly without going through methodcaller.
pythongh-127065: Make `methodcaller` thread-safe in free threading build
colesbury commented on Nov 21, 2024
I think the optimized vectorcall implementation from #107201 is not reentrant or thread-safe (even with the GIL) because the shared vectorcall_args array is modified during each invocation:
cpython/Modules/_operator.c
Line 1661 in 3926842
Most (or all) of our current vectorcall implementations extract out args[0] early, but that's not guaranteed and the vectorcall protocol is a public API.
For now, I'm planning to just disable the optimization in the free threading build (#127109), but I don't see an easy way of making it thread-safe, and I think we should consider reverting #107201 in 3.13 and main.
cc @eendebakpt @corona10 @vstinner
gh-127065: Make `methodcaller` thread-safe in free threading build (#…
pythongh-127065: Make `methodcaller` thread-safe in free threading bu…
[3.13] gh-127065: Make `methodcaller` thread-safe in free threading b…
eendebakpt commented on Nov 23, 2024
I agree this looks like a bug also in the normal build. The issue is hard to trigger in the normal build though, as indeed most vectorcall implementations (including the PyObject_VectorcallMethod used by the methodcaller) get args[0] before doing any work).
One option to make the vectorcall implementation thread-safe (and re-entrant) would be to copy the entire mc->vectorcall_args to a temporary buffer before setting the first element to args[0]. An implementation of this idea is main...eendebakpt:methodcaller_ft.
A quick benchmark (using the script from #107201) shows there is still a significant performance increase:
Current main:
Disable the vectorcall (this is the solution Sam added to the FT build):
Copy mc->vectorcall_args to a temporary buffer:
(all benchmarks without PGO and with the normal Python build)
@colesbury Is this idea an option for the free-threading build?
@Yhg1s What would you like to do for the 3.13 branch?
vstinner commented on Nov 25, 2024
Can you create a PR? I cannot open the link, it takes too long and GitHub displays an error.
eendebakpt commented on Dec 1, 2024
I removed the draft status from the PR (thanks to @colesbury for some good suggestions). Both PR #127109 or reverting #107201 are good options I think (a classic trade-off between performance and added complexity, except that in this case the status quo is not good)
gh-127065: Make methodcaller thread-safe and re-entrant (GH-127746)
eendebakpt commented on Jan 1, 2025
The issue has been addressed on main and on 3.13 the optimization has been removed for the free-threading build. I think we can close this issue, unless we would also like to address the issue for 3.13 with the normal build.
@Yhg1s Can you decide on 3.13?
pythongh-127065: Make methodcaller thread-safe and re-entrant (python…
pythongh-127065: Make `methodcaller` thread-safe in free threading bu…
kumaraditya303 commented on Apr 20, 2025
Closed as it is fixed in main branch.