Skip to content

Commit 6551bce

Browse files
authored
[mypyc] Enable partial, unsafe support for free-threading (#19167)
Enable multi-phase init when using a free-threaded (no-GIL) CPython build so we can enable proper multihreading. Work on mypyc/mypyc#1104. Work on mypyc/mypyc#1038. The implementation is still quite incomplete. We are missing synchronization in various places, so race conditions can cause segfaults. Only single-module compilation units are supported for now. Here's a toy benchmark I used to check that free threading works and can improve performance: ``` import sys import threading import time def fib(n: int) -> int: if n <= 1: return n else: return fib(n - 1) + fib(n - 2) NTHREADS = 6 print(f"Using {NTHREADS} threads") print(f"{sys._is_gil_enabled()=}") t0 = time.time() threads = [] for i in range(NTHREADS): t = threading.Thread(target=lambda: fib(36)) t.start() threads.append(t) for t in threads: t.join() print() print('elapsed time:', time.time() - t0) ```
1 parent 8c772c7 commit 6551bce

File tree

2 files changed

+50
-5
lines changed

2 files changed

+50
-5
lines changed

mypyc/codegen/emitmodule.py

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import json
99
import os
10+
import sys
1011
from collections.abc import Iterable
1112
from typing import Optional, TypeVar
1213

@@ -38,6 +39,7 @@
3839
)
3940
from mypyc.codegen.literals import Literals
4041
from mypyc.common import (
42+
IS_FREE_THREADED,
4143
MODULE_PREFIX,
4244
PREFIX,
4345
RUNTIME_C_FILES,
@@ -513,6 +515,9 @@ def __init__(
513515
self.use_shared_lib = group_name is not None
514516
self.compiler_options = compiler_options
515517
self.multi_file = compiler_options.multi_file
518+
# Multi-phase init is needed to enable free-threading. In the future we'll
519+
# probably want to enable it always, but we'll wait until it's stable.
520+
self.multi_phase_init = IS_FREE_THREADED
516521

517522
@property
518523
def group_suffix(self) -> str:
@@ -869,10 +874,31 @@ def generate_module_def(self, emitter: Emitter, module_name: str, module: Module
869874
"""Emit the PyModuleDef struct for a module and the module init function."""
870875
module_prefix = emitter.names.private_name(module_name)
871876
self.emit_module_exec_func(emitter, module_name, module_prefix, module)
877+
if self.multi_phase_init:
878+
self.emit_module_def_slots(emitter, module_prefix)
872879
self.emit_module_methods(emitter, module_name, module_prefix, module)
873880
self.emit_module_def_struct(emitter, module_name, module_prefix)
874881
self.emit_module_init_func(emitter, module_name, module_prefix)
875882

883+
def emit_module_def_slots(self, emitter: Emitter, module_prefix: str) -> None:
884+
name = f"{module_prefix}_slots"
885+
exec_name = f"{module_prefix}_exec"
886+
887+
emitter.emit_line(f"static PyModuleDef_Slot {name}[] = {{")
888+
emitter.emit_line(f"{{Py_mod_exec, {exec_name}}},")
889+
if sys.version_info >= (3, 12):
890+
# Multiple interpreter support requires not using any C global state,
891+
# which we don't support yet.
892+
emitter.emit_line(
893+
"{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},"
894+
)
895+
if sys.version_info >= (3, 13):
896+
# Declare support for free-threading to enable experimentation,
897+
# even if we don't properly support it.
898+
emitter.emit_line("{Py_mod_gil, Py_MOD_GIL_NOT_USED},")
899+
emitter.emit_line("{0, NULL},")
900+
emitter.emit_line("};")
901+
876902
def emit_module_methods(
877903
self, emitter: Emitter, module_name: str, module_prefix: str, module: ModuleIR
878904
) -> None:
@@ -905,11 +931,15 @@ def emit_module_def_struct(
905931
"PyModuleDef_HEAD_INIT,",
906932
f'"{module_name}",',
907933
"NULL, /* docstring */",
908-
"-1, /* size of per-interpreter state of the module,",
909-
" or -1 if the module keeps state in global variables. */",
910-
f"{module_prefix}module_methods",
911-
"};",
934+
"0, /* size of per-interpreter state of the module */",
935+
f"{module_prefix}module_methods,",
912936
)
937+
if self.multi_phase_init:
938+
slots_name = f"{module_prefix}_slots"
939+
emitter.emit_line(f"{slots_name}, /* m_slots */")
940+
else:
941+
emitter.emit_line("NULL,")
942+
emitter.emit_line("};")
913943
emitter.emit_line()
914944

915945
def emit_module_exec_func(
@@ -927,6 +957,8 @@ def emit_module_exec_func(
927957
module_static = self.module_internal_static_name(module_name, emitter)
928958
emitter.emit_lines(declaration, "{")
929959
emitter.emit_line("PyObject* modname = NULL;")
960+
if self.multi_phase_init:
961+
emitter.emit_line(f"{module_static} = module;")
930962
emitter.emit_line(
931963
f'modname = PyObject_GetAttrString((PyObject *){module_static}, "__name__");'
932964
)
@@ -958,7 +990,10 @@ def emit_module_exec_func(
958990

959991
emitter.emit_line("return 0;")
960992
emitter.emit_lines("fail:")
961-
emitter.emit_lines(f"Py_CLEAR({module_static});", "Py_CLEAR(modname);")
993+
if self.multi_phase_init:
994+
emitter.emit_lines(f"{module_static} = NULL;", "Py_CLEAR(modname);")
995+
else:
996+
emitter.emit_lines(f"Py_CLEAR({module_static});", "Py_CLEAR(modname);")
962997
for name, typ in module.final_names:
963998
static_name = emitter.static_name(name, module_name)
964999
emitter.emit_dec_ref(static_name, typ, is_xdec=True)
@@ -980,6 +1015,12 @@ def emit_module_init_func(
9801015
declaration = f"PyObject *CPyInit_{exported_name(module_name)}(void)"
9811016
emitter.emit_lines(declaration, "{")
9821017

1018+
if self.multi_phase_init:
1019+
def_name = f"{module_prefix}module"
1020+
emitter.emit_line(f"return PyModuleDef_Init(&{def_name});")
1021+
emitter.emit_line("}")
1022+
return
1023+
9831024
exec_func = f"{module_prefix}_exec"
9841025

9851026
# Store the module reference in a static and return it when necessary.

mypyc/common.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@
8888
# some details in the PEP are out of date.
8989
HAVE_IMMORTAL: Final = sys.version_info >= (3, 12)
9090

91+
# Are we running on a free-threaded build (GIL disabled)? This implies that
92+
# we are on Python 3.13 or later.
93+
IS_FREE_THREADED: Final = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
94+
9195

9296
JsonDict = dict[str, Any]
9397

0 commit comments

Comments
 (0)