Skip to content

Commit

Permalink
Trying out free-threaded Python on macOS
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw authored Jul 13, 2024
1 parent 6cf4849 commit 9feeaca
Showing 1 changed file with 114 additions and 0 deletions.
114 changes: 114 additions & 0 deletions python/trying-free-threaded-python.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Trying out free-threaded Python on macOS

Inspired by [py-free-threading.github.io](https://py-free-threading.github.io/) I decided to try out a beta of Python 3.13 with the new free-threaded mode enabled, which removes the GIL.

## Installation

I chose to use the macOS installer to get a pre-built binary. I downloaded and ran the macOS installer from [www.python.org/downloads/release/python-3130b3/](https://www.python.org/downloads/release/python-3130b3/), and then when I got to this screen:

![Installation dialog with a Customize button at the bottom](https://github.com/user-attachments/assets/7e57d8f1-6a4b-4551-babd-127317dff5cd)

I selected the "Customize" option and checked this additional box:

![Customize screen - I have checked the Free-threaded Python (experimental) option](https://github.com/user-attachments/assets/5aa8d4dd-5c70-493e-a183-2f0799079830)

Once it had finished installing I didn't run the script to add it to my path (I didn't want to intefere with my many other Python versions).

This gave me two new Python binaries - one with free-threading enabled and one without:

- `/Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13`
- `/Library/Frameworks/PythonT.framework/Versions/3.13/bin/python3.13t`

Note the `PythonT.framework` in the second path.

It also seemed to setup the following aliases in `/usr/local/bin`:

- `/usr/local/bin/python3.13`
- `/usr/local/bin/python3.13t`

These are symlinks:
```bash
ls -lah /usr/local/bin | grep python3.13
```
```
lrwxr-xr-x 1 root wheel 73B Jul 12 16:26 python3.13 -> ../../../Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13
lrwxr-xr-x 1 root wheel 80B Jul 12 16:26 python3.13-config -> ../../../Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13-config
lrwxr-xr-x 1 root wheel 81B Jul 12 16:26 python3.13-intel64 -> ../../../Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13-intel64
lrwxr-xr-x 1 root wheel 75B Jul 12 16:26 python3.13t -> ../../../Library/Frameworks/PythonT.framework/Versions/3.13/bin/python3.13t
lrwxr-xr-x 1 root wheel 82B Jul 12 16:26 python3.13t-config -> ../../../Library/Frameworks/PythonT.framework/Versions/3.13/bin/python3.13t-config
```

Starting those Python processes shows which one has free-threading in the interpreter header:
```
% /usr/local/bin/python3.13
Python 3.13.0b3 (v3.13.0b3:7b413952e8, Jun 27 2024, 09:57:31) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
% /usr/local/bin/python3.13t
Python 3.13.0b3 experimental free-threading build (v3.13.0b3:7b413952e8, Jun 27 2024, 10:04:51) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
```

## Testing it out

I asked Claude 3.5 Sonnet to write me [a quick test script](https://claude.site/artifacts/92aec50b-43c7-463a-b8a1-5d5ea0600708), then iterated on and simplified the result myself until I got to this:

```python
import argparse
import time
from concurrent.futures import ThreadPoolExecutor


def cpu_bound_task(n):
"""A CPU-bound task that computes the sum of squares up to n."""
return sum(i * i for i in range(n))


def main():
parser = argparse.ArgumentParser(description="Run a CPU-bound task with threads")
parser.add_argument("--threads", type=int, default=4, help="Number of threads")
parser.add_argument("--tasks", type=int, default=10, help="Number of tasks")
parser.add_argument(
"--size", type=int, default=5000000, help="Task size (n for sum of squares)"
)
args = parser.parse_args()

print(f"Running {args.tasks} tasks of size {args.size} with {args.threads} threads")

start_time = time.time()
with ThreadPoolExecutor(max_workers=args.threads) as executor:
list(executor.map(cpu_bound_task, [args.tasks] * args.size))
end_time = time.time()
duration = end_time - start_time

print(f"Time with threads: {duration:.2f} seconds")


if __name__ == "__main__":
main()
```
I saved this as `gildemo.py` and tried running it with the new Python binaries the one with free-threading enabled and the one without.

Here's what I saw in Activity Monitor while running the scripts:

No free-threading (with the GIL) - reported 99% CPU usage:

![Activity Monitor window showing a Python process using 99.2% CPU with 5 threads. Terminal visible above running a script: "Running 10 tasks of size 10000000 with 4 threads"](https://github.com/user-attachments/assets/686e2dcd-daff-4cfe-ba1e-c4aa38b0a10a)

Free-threading (no GIL) - reported 258.8% CPU usage:

![Activity Monitor window showing a PythonT process using 258.8% CPU with 5 threads. Terminal visible above running a script: "Running 10 tasks of size 10000000 with 4 threads".](https://github.com/user-attachments/assets/e7b8d291-a8a1-4e98-a73f-880403e14e8c)

And here are some results from running the script. First with the GIL in place:
```
% python3.13 gildemo.py --size 1000000
Running 10 tasks of size 1000000 with 4 threads
Time with threads: 17.14 seconds
```
And then without the GIL:
```
% python3.13t gildemo.py --size 1000000
Running 10 tasks of size 1000000 with 4 threads
Time with threads: 8.19 seconds
```

0 comments on commit 9feeaca

Please sign in to comment.