gh-116167 : autoriser la désactivation du GIL
(github.com/python)- La PR CPython #116338 a fusionné dans
python:mainune modification permettant de désactiver le GIL dans une build free-threaded avecPYTHON_GIL=0ou-X gil=0 - Pour conserver la possibilité de réactiver le GIL à l’exécution, les structures de données liées au GIL sont initialisées comme d’habitude, et la désactivation est gérée en définissant un flag au démarrage afin que
take_gil()etdrop_gil()retournent immédiatement - Lors des premières vérifications, avec
PYTHON_GIL=0, certains tests et petits programmes n’utilisant pas de threads ont fonctionné normalement, et des programmes multithread très basiques ont parfois marché, mais l’ensemble de la suite de tests a rapidement crashé danstest_asyncio - Pendant la revue, des tests pour
PYTHON_GIL, la documentation, l’option-X gilet sa prise en compte danssys.flagsont été ajoutés, et le traitement de la configuration a aussi été corrigé pour quePYTHON_GIL=1force bien l’activation du GIL - Les travaux de suivi ont été séparés entre la réactivation du GIL lors du chargement d’extensions incompatibles et la désactivation du GIL par défaut, et cette modification ajoute une surface de contrôle du GIL dans la build free-threaded de Python 3.13
Modification fusionnée
- La PR CPython #116338 traite du changement
gh-116167: Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0 colesburyl’a fusionnée danspython:mainle 11 mars 2024- L’ampleur du changement est indiquée comme 12 fichiers, 163 lignes ajoutées et 1 ligne supprimée
- La fonctionnalité visée est une option d’exécution permettant de désactiver le GIL dans une build free-threaded, et non dans une build classique
Mode de désactivation du GIL
- Dans une build free-threaded, le GIL peut être désactivé avec la configuration suivante
PYTHON_GIL=0-X gil=0
- Afin de pouvoir réactiver le GIL à l’exécution, toutes les structures de données liées au GIL sont initialisées comme d’habitude
- La désactivation effective repose sur la définition d’un flag au démarrage
- À cause de ce flag,
take_gil()etdrop_gil()retournent immédiatement
- À cause de ce flag,
- Pendant la revue, un commit corrigeant la configuration de
enable_gillorsquePYTHON_GIL=1a aussi été ajouté
Tests et limites actuelles
- Certains tests et petits programmes ont été vérifiés avec
PYTHON_GIL=0- Les tests et petits programmes n’utilisant pas de threads ont été confirmés comme fonctionnant normalement
- Des programmes multithread très basiques fonctionnaient parfois
- L’ensemble de la suite de tests a crashé rapidement, l’emplacement signalé étant
test_asyncio - Des tests de builders liés à NoGIL ont été planifiés à plusieurs reprises avec la commande
!buildbot nogilx86-64 MacOS Intel ASAN NoGIL PRx86-64 MacOS Intel NoGIL PRARM64 MacOS M1 Refleaks NoGIL PRARM64 MacOS M1 NoGIL PRAMD64 Ubuntu NoGIL Refleaks PRAMD64 Ubuntu NoGIL PRAMD64 Windows Server 2022 NoGIL PR
Portée ajoutée pendant la revue
corona10a suggéré qu’il serait utile d’ajouter des tests de variable d’environnement dansLib/test/test_cmd_line.py- Les commits suivants ont ensuite été ajoutés
Add test for PYTHON_GIL in test_cmd_lineSet enable_gil properly when PYTHON_GIL=1Don't add 'enable_gil' to test_embed in normal builds
colesburya estimé qu’il valait mieux documenter la variable d’environnement au moment de son ajout- En s’appuyant sur le fait que le flag de configuration
--disable-gilétait déjà documenté - Il a précisé que la documentation devait inclure le fait qu’elle n’est utilisable que dans les builds free-threaded, que
0force la désactivation du GIL, que1force l’activation du GIL, et qu’il s’agit d’une nouveauté de Python 3.13
- En s’appuyant sur le fait que le flag de configuration
- Le commit
Document PYTHON_GIL environment variablea ensuite été ajouté
Ajout de l’option -X gil et fusion finale
- Après une discussion sur Discord, il a été décidé d’ajouter aussi une option
-Xutilisable avec la variable d’environnement - Le titre de la PR a été modifié, passant d’une formulation ne traitant que de
PYTHON_GIL=0à une formulation incluantPYTHON_GIL=0 or -X gil=0 - Les commits ajoutés comprennent les éléments suivants
Add -X gil option, add to sys.flags, modify test to cover env var… and optionFix link to -X gilFix PYTHON_GIL versionchanged lineClarify test_flags in normal builds
ericsnowcurrently,erlend-aasland,corona10etcolesburyont approuvé la modification- Le commit de fusion est
2731913et, après la fusion,vstinnera réagi en disant que ce changement était « intéressant et très effrayant »
Travaux de suivi
- Deux tâches ont été séparées dans des issues de suivi
- La PR actuelle ne modifie pas la valeur par défaut du GIL ; elle permet aux utilisateurs de contrôler l’état du GIL dans une build free-threaded via une variable d’environnement ou une option
-X
1 commentaires
Commentaires Hacker News
Pour ceux que le travail sur no-GIL intéresse, voici quelques liens supplémentaires : [0], [1]
[0] Multithreaded Python without the GIL
https://docs.google.com/document/d/18CXhDb1ygxg-YXNBJNzfzZsD...
[1] Dépôt Github
https://github.com/colesbury/nogil
[0] https://peps.python.org/pep-0703/
[1] https://github.com/colesbury/nogil-3.12
J’ai hâte de voir à quel point le Python de base pourra encore être accéléré. La proposition de valeur de Python est aussi mise au défi par la multiplication des outils qui tentent d’atténuer ce problème
Parmi les outils d’accélération, Mojo, pytorch, triton, numba et taichi me viennent à l’esprit. Il y a tellement de tentatives pour résoudre ce problème que, la dernière fois que j’ai voulu en essayer un, j’ai été submergé par le nombre d’options. J’ai finalement choisi taichi ; c’était assez amusant et facile à utiliser, mais son champ d’application restait quelque peu limité
Taichi est vraiment sous-estimé. Il fonctionne sur toutes les plateformes, y compris Metal, propose beaucoup d’exemples et permet d’écrire du code facilement. Surtout, il s’intègre à l’écosystème sans le remplacer
https://github.com/taichi-dev
Une excellente vidéo de démonstration montrant ce qu’on peut faire avec Taichi : https://www.youtube.com/watch?v=oXRJoQGCYFg
https://www.youtube.com/watch?v=WNh4Q7-OSJs
https://www.taichi-lang.org/
Je me demande pourquoi l’approche de comptage de références biaisé décrite dans https://peps.python.org/pep-0703/ ne conserve une affinité que pour un seul thread, et exige des incréments/décréments atomiques dès qu’un autre thread y accède
Dans d’autres implémentations, par exemple plusieurs crates Rust qui implémentent le comptage de références biaisé, j’ai vu une approche où l’on n’incrémente atomiquement que lors du déplacement vers un nouveau thread ; ce thread effectue ensuite des incréments/décréments non atomiques jusqu’à revenir à 0, puis effectue un décrément atomique final. Je me demande si c’est parce qu’il s’agit d’un ajout à un système existant, avec un seul PyObject, et qu’on ne peut pas le remplacer pour pointer vers un nouvel objet local au thread
En Rust, le « move » pour transférer la propriété fait partie du langage, mais en C ou en Python il n’existe pas de concept équivalent, ce qui rend difficile de déterminer quand transférer la propriété et quel thread doit devenir le nouveau propriétaire. On pourrait utiliser des heuristiques. Par exemple, quand un objet est placé dans une queue.SimpleQueue, on pourrait abandonner ou transférer sa propriété, mais même dans ce cas il est difficile de savoir à l’avance quel thread fera le « get » de l’objet dans la file
Le gain de performance serait probablement faible. Beaucoup d’objets ne sont accédés que depuis un seul thread, certains objets sont accédés depuis plusieurs threads, mais les objets qui sont accédés exclusivement par un thread puis, plus tard, exclusivement par un autre thread sont rares
J’ai d’abord lu la nouvelle sur tranched bread, et maintenant ça ? Quelle époque incroyable
J’avais été un peu déçu quand le projet Unladen Swallow [1] s’est essoufflé. C’est agréable de voir Python revenir sur une trajectoire d’optimisation de fond
[1] https://en.wikipedia.org/wiki/CPython#Unladen_Swallow
J’aimerais qu’on me l’explique comme si j’avais cinq ans
Je comprends conceptuellement ce qu’est le GIL. Mais quel est l’impact de ce changement ? Faut-il s’attendre à une amélioration générale des performances, tandis que des packages vont désormais casser ?
Même sans tâches CPU très intensives, ce changement peut être utile. De nos jours, beaucoup de code est écrit avec les fonctionnalités natives asyncio de Python. Comme NodeJS, cela fonctionne sur un seul thread en cédant l’exécution via async/await, et un seul thread peut déjà atteindre un débit assez bon, de l’ordre de milliers de requêtes par seconde
Mais le gros problème est que dès qu’une tâche CPU s’exécute, elle bloque toutes les autres coroutines, ce qui provoque toutes sortes de problèmes obscurs et dégrade le nombre de requêtes par seconde. Par exemple, on peut observer des timeouts d’E/S aléatoires dans une coroutine, alors que la vraie cause est qu’une toute autre coroutine a accaparé le CPU pendant un court instant. Il est aussi très difficile d’observer pourquoi cela se produit. asyncio fournit la fonction
asyncio.to_thread()[1], qui aide à déplacer les tâches bloquantes hors du thread principal, mais à cause du GIL elle ne permet pas réellement d’isoler les tâches centrées sur le CPU de façon à ce qu’elles n’interfèrent pas avec les autres coroutines[1] https://docs.python.org/3/library/asyncio-task.html#asyncio....
Pour ceux que ça intéresse, GIL signifie Global Interpreter Lock
Y a-t-il une bonne ressource qui résume la situation d’ensemble ?
J’attends enfin avec impatience les benchmarks des différents outils