3 ms·
Yes, numpy does release the GIL. But the code in question has multiple numpy calls, called in a loop: sum((np.sin(np.cos(np.sin(np.cos(x + i)))).sum()
by quietbritishjim 2mo ago
Yes, numpy does release the GIL. But the code in question has multiple numpy calls, called in a loop:
sum((np.sin(np.cos(np.sin(np.cos(x + i)))).sum()
for i in range(n_loop)))
This is not like:
release GIL
# i = 0
compute y = x + 0 (numpy broadcasting sum)
compute z = np.cos(y) (elementwise)
...
add to running total
# i = 1
compute y = x + 1
...
reacquire GIL
Instead it is:
# i = 0
look up "+" operation
release GIL
compute y = x + 0
reacquire GIL
look up "np.cos" operation
release GIL
compute z = np.cos(y)
reacquire GIL
...
# i = 1
look up "+" operation
release GIL
compute y = x + 1
reacquire GIL
...
So there was work being protected by the GIL, that suddenly is exposed to lock contention with free threading.
Of course, without free threading, the lock contention would be way worse, but this time the GIL is the lock being contended. Numpy has to reacquire the GIL whenever it returns from a function call, and this expression is made up of multiple calls. To multithread effectively with numpy (in non-freethreading) you'd normally aim to vectorise into a small number of calls in big arrays.
The composition of +, then np.cos, etc. is not too bad if these are big arrays, but the problem is the pure Python iteration over the range which is, presumably, quite large. You could vectorise over the range:
x[..., None] + np.arange(n_loop, dtype=np.float64)
but this is the start of a new conversation.
- frollogaston 2mo agoAh ok, the times I did this and saw full CPU utilization was on pretty large arrays, so there was a lot less time spent in the GIL.
- ngoldbaum 2mo agoThere’s also the fact that INCREF and DECREF on shared objects is a lot more expensive than plain integer addition, so stuff becomes a bottleneck that was never a bottleneck. Kumar also fixed a bottleneck caused by a lock added only for safety on the free-threaded build. It’s hard to tell in advance than a fancy lock-free data structure is needed for something.
- quietbritishjim 2mo agoYou're talking about the same thing as the article: why were things not as fast as they could be in free threaded mode? But this comment thread is more of a meta discussion: why would lock contention issues only show up now, in free threaded mode, if numpy already released the GIL anyway? That's what I answered above: yes the GIL was sometimes released by numpy, but these operations really were happening with the GIL locked (in non free threaded build).
- ngoldbaum 2mo agoI’m saying that it’s a new issue on the free-threaded build.
- quietbritishjim 2mo agoRight. The original commenter already seemed to understand that. It's a new issue because the reference count needs locking, whereas previously it didn't because it was implicitly synchronised due to the GIL being locked. But the original commenter asked: hang on, I thought the GIL wasn't locked for numpy? Now you have reached the start of the conversation.