Le GIL de Python : le verrou qui a freiné un langage et comment il est enfin en train d'être supprimé
Khaled AuwadRésumé
Cet article explore l'histoire, les mécanismes et l'avenir du Global Interpreter Lock (GIL) de Python. Il trace les origines du GIL depuis les débuts de Python jusqu'à son rôle à la fois comme simplificateur et comme goulot d'étranglement. Il explique comment le GIL fonctionne au niveau C, pourquoi il empêche la parallélisation réelle, et comment Python 3.13 et 3.14 ont introduit des builds free-threaded en tant qu'expérimentaux et officiellement supportés, respectivement. L'article discute également des défis liés à la suppression du GIL, des compromis de performance et des implications pratiques pour les développeurs Python.
Introduction : pourquoi le GIL devrait vous intéresser ?
Si vous avez déjà essayé d'accélérer un programme Python en répartissant la charge de travail sur plusieurs threads, pour finalement constater qu'il s'exécutait exactement aussi lentement qu'avant, vous avez déjà rencontré le Global Interpreter Lock ou le GIL. Pendant plus de trois décennies, ce mécanisme a été à la fois le plus grand facteur de simplification de Python et sa limitation la plus frustrante. C'est la raison pour laquelle votre code si bien multithreadé, ne pouvait pas exploiter plus d'un cœur CPU à la fois, et c'est la raison pour laquelle les développeurs Python ont longtemps eu recours à des solutions de contournement comme le multiprocessing, Cython ou la réécriture des boucles intensives en C.
Mais un événement extraordinaire s'est produit. Python 3.13, sorti en octobre 2024, a introduit une version multithreading libre (free-threading) en tant que fonctionnalité expérimentale : une version de CPython, l'implémentation officielle de Python, capable de fonctionner avec le GIL désactivé. Python 3.14, sorti en octobre 2025, a ensuite fait du free-threading une option de compilation officiellement prise en charge dans le cadre de la Python Enhancement Proposal (PEP) 779. Il ne s'agit ni d'un fork, ni d'un patch, ni d'une expérience menée par un tiers. Cette fonctionnalité est directement intégrée à l'interpréteur. L'ère du GIL obligatoire est révolue, et Python entre dans un nouveau chapitre de son histoire.
Cet article de blog s'adresse aux développeurs qui ont peut-être entendu parler du GIL, ou non, et qui souhaitent en savoir plus à son sujet : ce que c'est, pourquoi il existait et ce que sa suppression implique pour leur code. Nous retracerons toute l'histoire : des décisions de conception initiales du début des années 1990, en passant par des décennies de frustration et de solutions de contournement créatives, jusqu'aux avancées techniques qui ont finalement rendu possible le free-threading. Au fil de cet article, nous examinerons des exemples de code concrets, nous verrons comment les bibliothèques réputées s'adaptent et nous explorerons ce que ce changement implique pour le machine learning et l'inférence LLM.
Qu'est-ce que le GIL, exactement ?
Le Global Interpreter Lock, ou GIL, est un mutex (mutual exclusion: un verrou d'exclusion mutuelle) qui protège l'accès aux objets Python, empêchant plusieurs threads d'exécuter des bytecodes Python simultanément. Concrètement, quel que soit le nombre de cœurs CPU de votre machine et le nombre de threads que votre programme Python crée, un seul thread peut exécuter du code Python à un instant donné. Le GIL est acquis avant qu'un thread puisse s'exécuter, et libéré lorsque le thread se met en pause, attend une opération d'I/O (input/output) ou termine son quantum de temps.
Voici une analogie : imaginez un bureau dont la clé est partagée entre plusieurs personnes. Toute personne souhaitant travailler dans ce bureau doit prendre la clé, faire son travail, et rendre la clé avant que quelqu'un d'autre puisse utiliser le bureau. Vous pouvez avoir dix employés (threads), mais un seul peut travailler à la fois. Les neuf autres attendent. C'est précisément ce qui se passe dans CPython lorsque vous créez plusieurs threads pour des tâches gourmandes en ressources CPU (CPU-bound).
Le code suivant illustre le problème. Nous lançons quatre threads qui comptent chacun jusqu'à 50 millions, en espérant un gain de vitesse d'environ 4x sur une machine à 4 cœurs. Au lieu de cela, la version multithreadée met à peu près le même temps que la version monothread, car le GIL force les threads à s'exécuter tour à tour plutôt qu'en parallèle.
import threading, time
def count_down(n):
while n > 0:
n -= 1
# Monothread
start = time.perf_counter()
count_down(50_000_000)
count_down(50_000_000)
count_down(50_000_000)
count_down(50_000_000)
single = time.perf_counter() - start
# Multithread (4 threads)
start = time.perf_counter()
threads = [
threading.Thread(target=count_down, args=(50_000_000,))
for _ in range(4)
]
for t in threads: t.start()
for t in threads: t.join()
multi = time.perf_counter() - start
print(f"Monothreaded: {single:.2f}s")
print(f"Multithreaded: {multi:.2f}s")
# Output sur un build avec le GIL:
# Monothreaded: 2.51s
# Multithreaded: 2.45s <-- Pas de gain de vitesse significatif !
Exemple de code 1 : le GIL empêche le vrai parallélisme dans le code CPU-bound multithreadé.
Comme vous pouvez le constater, la version multithreadée n'est pas plus rapide. Elle peut même légèrement être plus lente dans certains cas, en raison de l'overhead lié au context switching et à la contention du verrou. C'est le GIL en action : il sérialise l'exécution, rendant les threads inutiles pour le parallélisme CPU-bound. Pour les tâches I/O-bound (requêtes réseau, lectures de fichiers, requêtes en base de données), le GIL est libéré pendant l'attente, donc les threads fonctionnent correctement. Pour du code CPU-bound écrit en Python pur, les threads n'apportent pas de gain de vitesse sous le GIL. En revanche, des extensions C qui libèrent explicitement le GIL peuvent exécuter du calcul intensif en parallèle via plusieurs threads. La limitation du GIL concerne l'exécution des bytecodes Python, pas tous les calculs effectués dans un programme Python.
Pourquoi le GIL a-t-il été créé au départ ?
CPython, l’interpréteur officiel de Python, est le programme qui lit vos fichiers .py et les exécute. Il est écrit en C, d’où son nom de C‑Python. Il comprend le moteur d’exécution Python, la bibliothèque standard et l’interpréteur de bytecode qui exécute votre code.
Pour comprendre pourquoi le GIL a été introduit, il faut remonter au début des années 1990, lorsque Guido van Rossum, le créateur de Python, concevait CPython. Les décisions prises à l'époque n'étaient pas des erreurs, c'étaient des compromis d'ingénierie pragmatiques qui ont rendu Python simple, sûr et suffisamment rapide pour le paysage informatique de l'époque. Examinons chaque raison en détail.
Gestion mémoire par comptage de références
Python fait partie de la famille des langages de programmation dotés d'une gestion automatique de la mémoire : lorsque vous écrivez du code Python, vous n'avez pas besoin d'allouer ou de libérer manuellement de la mémoire.
CPython utilise le comptage de références comme stratégie principale de gestion mémoire. Chaque objet Python possède un compteur de références ob_refcnt, incrémenté lorsqu'une nouvelle référence vers l'objet est créée et décrémenté lorsqu'une référence est supprimée. Lorsque le compteur atteint zéro, l'objet est immédiatement désalloué. Cette approche est simple, déterministe et efficace pour les programmes monothreads. Cependant, dans un environnement multithread, plusieurs threads pourraient incrémenter ou décrémenter simultanément le même compteur de références, entraînant des race conditions : les objets pourraient être libérés prématurément (provoquant des crashes) ou ne jamais l'être (provoquant des fuites mémoire).
Le GIL résout cet problème de manière élégante, en garantissant qu'un seul thread à la fois puisse exécuter du code Python. Cela rend les modifications du compteur de références intrinsèquement sûres, éliminant ainsi la nécessité d'un verrou distinct sur chaque objet. L'alternative aurait consisté à ajouter un verrou fin au compteur de références de chaque objet (verrouillage à grains fins), ce qui aurait été extrêmement complexe et aurait considérablement ralenti l'interpréteur pour les programmes à thread unique, qui constituaient alors la grande majorité des cas d'utilisation de Python.
La vidéo ci-dessous montre : 1 comment le compteur de références est modifié lors de la création et de la destruction d'objets, 2 comment le problème se manifesterait si Python ne disposait pas de GIL et si plusieurs threads devaient modifier simultanément le même compteur de références, 3 comment le GIL empêche ce problème :
Simplicité pour les extensions C
L'une des grandes forces de Python a toujours été son API d'extension C, qui permet aux développeurs d'écrire des modules haute performance en C et de les appeler depuis Python. Le GIL a considérablement simplifié le développent de ces extensions. Les auteurs d'extensions n'avaient pas à se soucier de la thread safety lors de la manipulation d'objets Python, car le GIL garantissait qu'un seul thread exécuterait du code Python. Cela a facilité l'accès au développement d'extensions et a contribué au riche écosystème de calcul scientifique, de traitement d'image et de bibliothèques system-level dont Python bénéficie aujourd'hui. Sans le GIL, chaque extension C aurait dû implémenter sa propre stratégie de verrouillage, une tâche réputée pour être source d'erreurs.
Performance monothread
Paradoxalement, le GIL peut améliorer les performances monothreads. Le verrouillage à grains fins (acquérir et libérer de nombreux petits verrous au cours de l'exécution) génère un overhead : chaque opération de verrouillage implique des instructions atomiques, une invalidation de cache et des pipeline stalls potentiels. En utilisant un seul verrou global, CPython évite entièrement cet overhead dans le cas d'usage le plus courant. Dans les années 1990, les machines mono-processeur étaient la norme, et le coût du verrouillage à grains fins aurait rendu Python plus lent pour la quasi-totalité des utilisateurs. Le GIL était un choix pragmatique : optimiser pour 99% des cas d'usage au détriment du 1% qui avait besoin de vrai parallélisme.
Prévention des deadlocks
Le verrouillage à grains fins introduit le risque de deadlocks : le thread A détient le verrou 1 et attend le verrou 2, tandis que le thread B détient le verrou 2 et attend le verrou 1. Les deux threads se bloquent indéfiniment. Le GIL réduisait une partie de cette complexité à l'intérieur même de CPython, car l'interpréteur avait besoin de moins de coordination interne entre verrous. Cela dit, le GIL n'a jamais rendu les deadlocks impossibles dans les programmes Python. Des deadlocks au niveau de l'application peuvent toujours se produire à cause de verrous créés par l'utilisateur, de variables de condition, de join() mal utilisés ou d'ordres d'acquisition incohérents. Ce que le GIL apportait surtout, c'était un point de départ plus simple pour un langage pensé pour être accessible et pour un écosystème d'extensions construit par des scientifiques et des ingénieurs qui n'étaient pas forcément des spécialistes de la programmation concurrente.
Les limitations : quand le GIL devient un plafond
Si les choix de conception du GIL étaient judicieux dans les années 1990, le monde de l'informatique a depuis connu une évolution considérable. Les processeurs multi-cœurs sont devenus la norme, les jeux de données ont atteint des tailles colossales, et Python s'est retrouvé au cœur du calcul scientifique, de l'analyse de données et du machine learning. Le GIL, autrefois simplification pratique, est devenu un goulot d'étranglement sérieux.
Le problème du multi-cœur
Les processeurs modernes ont couramment 4, 8, 16 cœurs ou plus. Le matériel serveur peut en compter 64 ou 128. Or, un programme CPython standard avec des threads CPU-bound ne peut utiliser qu'un seul de ces cœurs à la fois. Plus le nombre de cœurs augmente, plus le fossé s'accentue entre ce que le matériel offre et ce que Python peut exploiter. Il ne s'agit pas d'un simple inconvénient, mais d'une limitation architecturale fondamentale qui oblige les développeurs Python à recourir à des solutions de contournement de plus en plus complexes.
Solutions de contournement et leurs coûts
La communauté Python a développé plusieurs solutions au fil des années, chacune avec des compromis significatifs. L'approche la plus courante est le module multiprocessing qui crée des processus séparés du système d'exploitation, chacun avec son propre interpréteur Python et son propre GIL. Bien que cette approche permette le vrai parallélisme, elle s'accompagne d'un overhead important : la création de processus est coûteuse, la communication inter-processus nécessite de la sérialisation (pickling), et l'utilisation mémoire est multipliée car chaque processus possède sa propre copie du runtime Python et des modules chargés.
Le asyncio est une autre solution qui mérite d'être mentionnée, mais elle résout un problème différent. Certains développeurs confondent la concurrence avec le parallélisme, et une boucle d'événements avec l'exécution multicœur. Lorsque vous utilisez asyncio, au lieu de créer une exécution parallèle sur le processeur, ce module utilise une boucle d'événements (event loop) comme planificateur de tâches coopératif: de nombreuses tâches peuvent progresser au fil du temps, mais elles le font généralement sur un seul thread en lâchant (yield) le contrôle chaque fois qu'elles attendent une I/O. Cela rend asyncio idéal pour les charges de travail à forte concurrence et liées aux I/O (I/O-bound), telles que les serveurs web, les clients API et les services réseau. Cependant, il ne contourne pas le GIL et n'offre pas à lui seul un véritable parallélisme multicœur pour le code Python lié au CPU (CPU-bound). Si une tâche gourmande en ressources CPU (CPU-intensive) s'exécute directement dans la boucle d'événements, elle bloquera les autres tâches jusqu'à ce qu'elle soit terminée.
D'autres solutions consistent à écrire du code CPU-intensive en Cython, un sur-ensemble de Python dans lequel presque tout code Python valide est également valide en Cython. Cython agit comme un compilateur statique qui convertit les fichiers .pyx en code C, lequel est ensuite compilé en un module d'extension Python, tout en servant de passerelle vers le C et le C++ en permettant d'appeler des fonctions C, d'utiliser des types C et d'interagir directement avec des classes C++.
L'écriture de code CPU-intensive en Cython permet de libérer explicitement le GIL pour les calculs au niveau C, en utilisant des extensions C natives qui contournent le GIL pour les opérations internes, ou en déchargeant les calculs vers des services externes. Chacune de ces approches ajoute de la complexité, réduit la portabilité et compromet l'un des principaux atouts de Python : sa lisibilité et sa facilité d'utilisation.
import multiprocessing, time, threading
def count_down(n):
while n > 0:
n -= 1
N = 50_000_000
# Threading (limité par le GIL) -- pas de gain de vitesse
start = time.perf_counter()
threads = [threading.Thread(target=count_down, args=(N,))
for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
threaded_time = time.perf_counter() - start
# Multiprocessing (contourne le GIL) -- gain de vitesse réel
start = time.perf_counter()
procs = [multiprocessing.Process(target=count_down, args=(N,))
for _ in range(4)]
for p in procs: p.start()
for p in procs: p.join()
mp_time = time.perf_counter() - start
print(f"Threading: {threaded_time:.2f}s")
print(f"Multiprocessing: {mp_time:.2f}s")
# Output sur une machine à 4 cœurs (build avec GIL) :
# Threading: 2.51s
# Multiprocessing: 0.72s <-- exécution ~3.5x plus rapide !
Exemple de code 2 : le multiprocessing atteint le vrai parallélisme en utilisant des processus séparés, chacun avec son propre GIL.
Comme le montre l'exemple, le multitraitement apporte un réel gain de vitesse, mais au prix d'une consommation de mémoire plus importante et d'une gestion plus complexe des processus distincts. Certains mécanismes de communication interprocessus, tels que les files d'attente (queues) et les canaux (pipes), nécessitent la sérialisation et la transmission des données, ce qui ajoute une surcharge et limite les types d'objets pouvant être partagés facilement. Cependant, Python fournit également des mécanismes de mémoire partagée directe tels que multiprocessing.Value, multiprocessing.Array, multiprocessing.sharedctypes, and multiprocessing.shared_memory. Pour de nombreuses applications, ces options atténuent cette contrainte, mais elles nécessitent une programmation plus minutieuse et ne prennent pas en charge les objets Python arbitraires aussi facilement que l'état partagé entre les threads.
La chronologie du free-threading : de l'expérimentation au support officiel
Le chemin vers un Python sans GIL a été long et méticuleusement orchestré. Il a impliqué des années de recherche, un prototype que beaucoup croyaient ne jamais être accepté, un PEP fondateur et un déploiement progressif conçu pour donner à l'écosystème le temps de s'adapter. Voici comment cela s'est déroulé.
| Année | Jalon | Signification |
|---|---|---|
| 2021 | Sam Gross présente le fork nogil | Un POC démontrant que CPython peut fonctionner sans le GIL avec des performances acceptables |
| 2022 | Meta sponsorise le projet nogil | Un soutien industriel signalant une intention sérieuse ; le fork gagne en visibilité communautaire |
| Jan 2023 | PEP 703 proposé | Proposition formelle de rendre le GIL optionnel dans CPython, avec un plan de déploiement en trois phases |
| Oct 2023 | PEP 703 accepté | Le Steering Council de Python approuve avec une approche progressive et réversible |
| Oct 2024 | Python 3.13 publié (Phase I) | Build free-threaded disponible comme fonctionnalité expérimentale ; non recommandé pour la production |
| Mar 2025 | PEP 779 créé | Définit les critères concrets pour promouvoir le free-threading du statut expérimental à supporté |
| Jun 2025 | PEP 779 finalisé | Critères remplis ; free-threading approuvé pour la Phase II |
| Oct 2025 | Python 3.14 publié (Phase II) | Free-threading officiellement supporté ; n'est plus expérimental ; le build avec GIL reste le défaut |
| Mar 2026 | PEP 803 proposé (en attente de décision du SC) | Propose abi3t, un ABI stable pour les builds free-threaded, ciblant Python 3.15 |
| Avr 2026 | Python 3.15 en alpha | Développement en cours des améliorations du JIT et de l'infrastructure free-threaded |
Tableau 1 : jalons clés de l'histoire du free-threading.
Phase I : Python 3.13 (expérimental)
Python 3.13, publié en octobre 2024, a été la première version à livrer un build free-threaded. Les utilisateurs pouvaient installer une variante séparée (python3.13t), c'est-à-dire un build de CPython capable de s'exécuter avec le GIL désactivé. Cela ne garantissait toutefois pas que le GIL serait toujours désactivé : il pouvait être réactivé à l'exécution, et il pouvait aussi se réactiver lors de l'import d'extensions C qui n'étaient pas encore déclarées compatibles avec le free-threading. Cette variante était explicitement qualifiée d'expérimentale : l'équipe Python ne garantissait pas la stabilité de l'API, les performances étaient encore en cours d'optimisation, et de nombreuses extensions C n'avaient pas encore été mises à jour. L'objectif principal de la Phase I était de permettre aux early adopters et aux mainteneurs de bibliothèques de tester leur code et d'amorcer le processus de migration.
Phase II : Python 3.14 (officiellement supporté)
Python 3.14, publié en octobre 2025, a marqué un jalon majeur. Le PEP 779 a établi des critères clairs pour que le build free-threaded soit promu du statut expérimental à officiellement supporté : un overhead monothread ne dépassant pas 15% par rapport au build avec GIL, un overhead mémoire ne dépassant pas 20%, des API stables et une documentation complète. Ces critères ont été remplis, et le build free-threaded a accédé à la Phase II. Il est important de noter que le build avec GIL reste le build par défaut car le build free-threaded doit être sélectionné explicitement. La Phase III, qui ferait du free-threading le mode par défaut, est attendue dans une future version de Python.
Perspectives : Python 3.15 et au-delà
Python 3.15 est actuellement en développement alpha, avec une version finale attendue en octobre 2026. Le PEP 803, actuellement en cours d'examen par le Steering Council, propose abi3t, un ABI stable conçu spécifiquement pour les builds free-threaded, ce qui facilitera considérablement la livraison de wheels compatibles à la fois avec les builds GIL et free-threaded par les auteurs d'extensions. Les travaux en cours sur le compilateur JIT laissent entrevoir de nouvelles améliorations de performances. La trajectoire est claire : la communauté Python est engagée sur la voie du free-threading, et chaque version nous rapproche d'un monde où le GIL est optionnel par défaut.
Comment fonctionne le free-threading : les fondements techniques
Supprimer le GIL ne se résume pas à effacer un verrou. Le GIL est tissé dans les couches les plus profondes des internals de CPython, et sa suppression nécessite de remplacer la sécurité qu'il procure par une combinaison de mécanismes plus granulaires et plus efficaces. Le build free-threaded utilise cinq techniques clés qui travaillent de concert pour assurer la thread safety sans le goulot d'étranglement global du GIL.
1. Comptage de références biaisé (Biased Reference Counting)
Le changement le plus critique concerne la gestion des compteurs de références. Dans le build avec GIL, les compteurs de références sont de simples entiers modifiés sans mécanisme de thread safety, car le GIL garantit un accès exclusif. Dans le build free-threaded, chaque objet possède deux compteurs de références : un compteur local et un compteur partagé. Le thread propriétaire de l'objet (généralement celui qui l'a créé) modifie le compteur local au moyen d'instructions rapides non atomiques. Les autres threads modifient le compteur partagé au moyen d'instructions atomiques plus lentes. Cette approche, fondée sur un article de recherche de 2018, garantit que le cas courant (un thread accédant à ses propres objets) reste rapide, tandis que le cas plus rare (un accès inter-threads) est sûr mais légèrement plus lent.
2. Immortalisation
Certains objets sont accédés si fréquemment par tant de threads que même le comptage de références biaisé engendrerait une contention inacceptable. Les objets comme None, True, False et les petits entiers sont immortalisés : leurs compteurs de références ne sont jamais modifiés. Ils sont traités comme permanemment vivants. Cela élimine toute une catégorie de contention pour les objets les plus couramment partagés dans tout programme Python, et réduit significativement l'overhead du build free-threaded.
3. Comptage de références différé (Deferred Reference Counting)
Les variables locales sur la pile d'exécution (call stack) sont extrêmement courantes et ont une durée de vie très courte. Modifier les compteurs de références pour chaque variable de pile serait un gaspillage. Dans le build free-threaded, les variables locales sur la pile ne modifient pas immédiatement les compteurs de références. À la place, le garbage collector suit ces références séparément. Lorsque le garbage collector s'exécute, il met à jour les références de la pile. Cela réduit le volume global de modifications des compteurs de références, améliorant les performances sans sacrifier la correction.
4. Sections critiques par objet (Per-Object Critical Sections)
Dans le build avec GIL, la thread safety des types conteneurs (dicts, lists, sets) est garantie implicitement car un seul thread peut s'exécuter à la fois. Dans le build free-threaded, ces types utilisent des sections critiques à granularité fine : chaque conteneur possède son propre verrou qui protège son état interne. Ces verrous utilisent un protocole d'évitement de deadlocks, garantissant que les threads ne peuvent pas se retrouver en deadlock même en acquérant plusieurs verrous. Dans le cas courant, les lectures sont optimistes : un thread tente de lire sans acquérir de verrou, et s'il détecte un conflit, il retente. Cela rend les workloads à forte proportion de lectures rapides tout en garantissant la correction.
5. Réclamation basée sur les états de quiescence (QSBR)
Lorsqu'un thread retire un élément d'une structure de données partagée, d'autres threads peuvent encore détenir des références vers celui-ci. Libérer l'élément immédiatement pourrait provoquer des bugs de type use-after-free. Le QSBR résout ce problème en suivant le moment où tous les threads sont passés par un état de quiescence (un point où ils ne détiennent aucune référence interne). Ce n'est qu'après que tous les threads ont atteint un état de quiescence que la mémoire est réclamée. Cela permet des lectures sans verrou dans le cas courant tout en assurant une libération mémoire sécurisée. C'est la même technique utilisée dans les structures de données concurrentes haute performance des kernels de systèmes d'exploitation.
| Mécanisme | Ce qu'il remplace | Bénéfice clé |
|---|---|---|
| Comptage de références biaisé | Compteurs de réf. protégés par le GIL | Accès local rapide, accès inter-threads sûr |
| Immortalisation | Comptage de réf. pour les objets courants | Zéro contention pour None, True, False, petits entiers |
| Comptage de réf. différé | Comptage immédiat des réf. de pile | Réduction des modifications de compteurs pour les variables locales |
| Sections critiques par objet | GIL pour la sécurité des conteneurs | verrouillage à grains fins avec évitement des deadlocks |
| QSBR | GIL pour la réclamation mémoire | Lectures sans verrou avec libération mémoire sécurisée |
Tableau 2 : les cinq mécanismes remplaçant le GIL dans Python free-threaded.
Comment l'écosystème Python a réagi
La suppression du GIL n'affecte pas seulement l'interpréteur Python, mais l'ensemble de l'écosystème de bibliothèques et de frameworks qui s'appuient sur lui. De nombreuses bibliothèques réputées, en particulier dans le domaine du calcul scientifique et de la data science, possèdent des extensions C conçues en tenant compte du GIL. S'adapter à un monde sans GIL requiert un effort d'ingénierie considérable. Voici comment les acteurs majeurs réagissent.
NumPy
NumPy est sans doute la bibliothèque la plus essentielle de la stack Python scientifique, et ses développeurs se sont impliqués activement dans l'effort de free-threading dès le départ. Le cœur de NumPy est écrit en C, et de nombreuses opérations libèrent déjà le GIL pour le calcul interne, ce qui permet à du code C multithreadé de s'exécuter en parallèle. Cependant, la gestion des objets au niveau Python dépendait encore du GIL. NumPy 2.1 a introduit un support expérimental des builds free-threaded, et les versions ultérieures ont amélioré la compatibilité. Le défi principal est de garantir que le comptage de références et la gestion du cycle de vie des objets NumPy soient thread-safe sans le GIL. Début 2026, NumPy dispose d'un support fonctionnel sur Python free-threaded, mais ce sujet continue encore d'évoluer et d'être optimisé.
Pandas
Pandas, qui s'appuie sur NumPy, fait face à des défis similaires mais avec une complexité supplémentaire due à ses abstractions de plus haut niveau. L'équipe Pandas a travaillé sur la compatibilité free-threaded en parallèle des efforts de NumPy. De nombreuses opérations Pandas utilisent en interne des tableaux NumPy, la compatibilité de NumPy aide donc automatiquement. Cependant, Pandas gère également ses propres structures de données (DataFrames, Series, objets Index) en Python, ce qui nécessite un audit minutieux de la thread safety. Des wheels free-threaded sont apparues dans la branche 2.2.x, et Pandas 3.0 a prolongé ce travail, mais il reste plus juste de parler d'un support en amélioration continue que d'un état totalement stabilisé sur tous les workloads.
PyTorch
PyTorch occupe une position singulière. Il est principalement utilisé pour le machine learning, où les calculs lourds s'exécutent déjà dans des kernels C++ et CUDA qui libèrent le GIL. Pour les utilisateurs de PyTorch, le GIL a longtemps été une préoccupation surtout lors des phases Python de chargement de données, de construction de modèles et d'orchestration de l'entraînement. Le backend C++ de PyTorch fonctionne déjà sans le GIL pour les opérations sur les tenseurs, et l'équipe travaille aussi à améliorer la compatibilité de la couche Python. Le support de Python free-threaded progresse graduellement, mais reste encore expérimental ou en preview dans les versions actuelles de PyTorch. En pratique, le free-threading pourrait simplifier considérablement le pipeline de chargement de données de PyTorch, qui utilise actuellement le multiprocessing pour contourner le GIL, avec un overhead mémoire élevé.
Autres bibliothèques majeures
De nombreuses autres bibliothèques sont à différents stades d'adaptation. SciPy, qui partage une grande partie de l'infrastructure de NumPy, progresse en parallèle. Matplotlib nécessite comparativement moins de changements. Flask et Django, qui sont en grande partie des frameworks web en Python pur, ont de bonnes chances de fonctionner sur des builds free-threaded si leur pile de dépendances est compatible, mais ils ne publient pas le même type de statut officiel que les projets riches en extensions C. Une page de suivi maintenue par la communauté, portée par Quansight Labs et des contributeurs open source, recense l'état de compatibilité de packages réputés avec extensions. En avril 2026, de nombreux packages largement utilisés ont déjà publié des wheels free-threaded ou ont des travaux actifs en cours, mais le niveau de support va de l'expérimental au validé.
| Bibliothèque | Statut actuel | Défi clé |
|---|---|---|
| NumPy | Support expérimental (2.1+), en amélioration | Comptage de réf. et cycle de vie des objets thread-safe dans le cœur C |
| Pandas | Wheels free-threaded disponibles, support en amélioration | Thread safety des structures de données Python |
| PyTorch | Support expérimental / preview | Thread safety à la frontière Python-C++ |
| SciPy | Support disponible, évolue avec NumPy | Infrastructure NumPy partagée |
| Matplotlib | Support disponible, avec réserves | Changements limités pour des workflows surtout orientés I/O |
| Flask / Django | Fonctionnement probable si les dépendances sont compatibles | Majoritairement en Python pur, mais sans déclaration formelle de compatibilité free-threaded |
Tableau 3 : statut de compatibilité free-threading des principales bibliothèques Python (avril 2026).
Impact sur le machine learning et l'inférence de LLM
Les praticiens du machine learning et les développeurs de LLM sont légitimement impatients de comprendre comment le free-threading affecte leur travail. La réponse est nuancée : l'impact dépend fortement de savoir si le goulot d'étranglement se situe au niveau de l'orchestration Python ou de l'exécution de kernels natifs.
Comment les frameworks ML gèrent le GIL actuellement
Les frameworks ML modernes comme PyTorch et TensorFlow contournent déjà le GIL pour la très grande majorité de leurs calculs. Lorsque vous appelez une multiplication matricielle sur un tenseur GPU, le calcul est délégué à un kernel CUDA qui s'exécute entièrement en dehors du GIL. Le GIL est libéré avant le lancement du kernel et réacquis après son achèvement. Cela signifie que pour les calculs purement sur GPU, le GIL est déjà largement sans importance. Cependant, il existe un manque critique : le code d'orchestration Python, y compris le chargement de données, le preprocessing, le calcul de métriques et le checkpointing, s'exécute toujours sous le GIL. C'est là que le free-threading peut faire une différence significative.
Entraînement : chargement et preprocessing parallèles des données
Pendant l'entraînement d'un modèle, le chargement et le preprocessing des données sont souvent le goulot d'étranglement. En pratique, on utilise souvent le DataLoader de PyTorch avec plusieurs worker processes via le multiprocessing, chacun chargeant et préparant les données avant de les envoyer au GPU. Cette approche fonctionne, mais elle a un coût significatif : la communication inter-processus impose de la sérialisation et, selon la plateforme et la méthode de démarrage des processus, les workers peuvent se retrouver avec des copies mémoire séparées des données. Sous Linux, le copy-on-write peut atténuer ce problème pour des jeux de données en lecture seule lorsque fork est utilisé, alors qu'avec des configurations basées sur spawn, la duplication mémoire est plus fréquente. Le free-threading rend plus crédibles les architectures de chargement de données basées sur des threads, ce qui peut réduire la duplication mémoire et l'overhead d'IPC, puisque les threads partagent nativement le même espace d'adressage. Les gains exacts dépendent fortement du workload, de la conception du dataset et de la plateforme ; il vaut donc mieux présenter cela comme une direction prometteuse que comme un benchmark universel.
Inférence de LLM : une opportunité d'une autre nature
L'inférence de grands modèles de langage (LLM) représente un cas particulièrement intéressant. Lors d'une inférence par batch, un serveur peut traiter des dizaines, voire des centaines de requêtes simultanément. Chaque requête implique une tokenisation au niveau Python, une construction de prompt et un décodage de la sortie, en plus du calcul GPU. Dans un processus Python limité par le GIL, ces tâches Python doivent être sérialisées, créant un goulot d'étranglement même lorsque le GPU n'est pas pleinement utilisé. Le free-threading permet à ces tâches d'orchestration de s'exécuter véritablement en parallèle sur les cœurs, améliorant le débit.
Il est important de bien comprendre ce que le free-threading change et ce qu'il ne change pas ici. Des bibliothèques comme HuggingFace Transformers et llama-cpp-python libèrent déjà le GIL pendant l'inférence de modèle sur GPU ou CPU, donc les forward passes lourdes en calcul n'ont jamais été le problème. Le bénéfice du free-threading est que l'orchestration Python autour de ces appels, notamment la tokenisation, l'assemblage des prompts, le décodage de la sortie et la gestion des résultats, peut désormais s'exécuter également en vrai parallèle. Sur un build avec GIL, ces étapes sont sérialisées même si le calcul GPU ne l'est pas, créant un goulot d'étranglement faible mais mesurable à forte concurrence. Pour le serving de LLM en production à grande échelle, des moteurs dédiés comme vLLM ou Text Generation Inference (TGI) gèrent le batching et le scheduling au niveau C++/CUDA, contournant entièrement le GIL. L'exemple de code ci-dessous utilise HuggingFace Transformers pour illustrer le pattern de threading.
# Inférence batch de LLM avec free-threading (HuggingFace Transformers)
import threading
from transformers import AutoTokenizer, AutoModelForCausalLM
def process_request(prompt, model, tokenizer, results, idx):
# Toutes ces étapes peuvent désormais s'exécuter en vrai parallèle
tokens = tokenizer.encode(prompt,
return_tensors='pt') # Niveau Python
output_ids = model.generate(tokens,
max_new_tokens=256) # GPU (GIL libéré)
text = tokenizer.decode(output_ids[0],
skip_special_tokens=True) # Niveau Python
results[idx] = text
def batch_inference(prompts):
model = AutoModelForCausalLM.from_pretrained(
'meta-llama/Llama-3.1-8B')
tokenizer = AutoTokenizer.from_pretrained(
'meta-llama/Llama-3.1-8B')
results = [None] * len(prompts)
# Vrais threads parallèles sur Python free-threaded !
threads = [
threading.Thread(
target=process_request,
args=(p, model, tokenizer, results, i)
)
for i, p in enumerate(prompts)
]
for t in threads: t.start()
for t in threads: t.join()
return results
Exemple de code 3 : avec le free-threading, l'orchestration Python lors de l'inférence de LLM s'exécute en vrai parallèle.
La vision réaliste
Il est important d'être réaliste sur l'ampleur de l'impact. Pour les workloads d'entraînement et d'inférence fortement GPU-bound (où le GPU est le goulot d'étranglement dans 95% du temps), le free-threading ne changera pas radicalement le débit global. Le calcul s'effectue déjà en dehors du GIL. Cependant, pour les workloads impliquant un traitement Python significatif, tels que les pipelines RLHF (Reinforcement Learning from Human Feedback), les systèmes LLM multi-agents ou le serving d'inférence avec du pré/post-processing complexe, le free-threading peut apporter des améliorations significatives tant en débit qu'en efficacité mémoire. Peut-être l'impact le plus important à long terme sera-t-il en termes de simplicité. Aujourd'hui, obtenir du parallélisme dans les pipelines ML nécessite des configurations multiprocessing complexes avec de la mémoire partagée, des pools de processus et une sérialisation minutieuse. Le free-threading permet d'utiliser le threading, simple et familier, pour ces tâches, réduisant la complexité du code et la surface d'exposition aux bugs. Ce n'est pas une amélioration éclatante sur un benchmark, mais c'est un gain qualitatif significatif pour les ingénieurs ML.
Ce que cela signifie pour vous : guide pratique
Si vous êtes développeur·se Python qui se demande comment tirer parti du free-threading, voici ce que vous devez savoir pour commencer dès aujourd'hui.
Comment essayer le free-threading
La façon la plus simple de commencer est d'installer le build free-threaded de Python 3.14. Sur de nombreuses plateformes, celui-ci est disponible sous forme de package séparé. Par exemple, avec le gestionnaire de versions pyenv ou l'installateur officiel Python, vous pouvez sélectionner la variante free-threaded. L'exécutable se nomme généralement python3.14t (notez le suffixe t), et il peut coexister avec le build GIL standard python3.14.
# Installer Python 3.14 free-threaded avec pyenv
pyenv install 3.14.0t
pyenv global 3.14.0t
# Ou avec l'installateur officiel (macOS/Windows) :
# Sélectionner l'option 'free-threaded' lors de l'installation
# Vérifier que vous exécutez Python free-threaded :
python3.14t -c "import sys; print(sys._is_gil_enabled())"
# Sortie : False
# Vérifier les threads disponibles :
python3.14t -c "import threading;
print(f'Threads disponibles : {threading.active_count()}')"
Exemple de code 4 : installation et vérification d'un build Python free-threaded.
Quand rester avec le build GIL
Le free-threading n'est pas le bon choix pour tous les projets, du moins pas encore. Si votre code est principalement I/O-bound (serveurs web, clients API, applications en base de données), vous ne verrez que peu de bénéfices du free-threading, car le GIL est déjà libéré pendant les opérations I/O. Si vous dépendez d'extensions C qui n'ont pas encore été mises à jour pour le free-threading, vous risquez des crashes ou des bugs subtils. Et si votre application est monothread, l'overhead d'environ 10% du free-threading le rend nettement inférieur au build avec GIL.
Checklist de migration
Si vous souhaitez migrer un projet existant vers le free-threading, suivez cette checklist:
- Installez le build free-threaded et exécutez votre suite de tests. Tout échec indique des problèmes de thread safety dans votre code ou vos dépendances.
- Auditez vos propres extensions C, si vous en avez, pour la thread safety. Recherchez les états partagés non protégés, les acquisitions de verrous manquantes et le code qui présuppose que le GIL est détenu.
- Vérifiez le statut de compatibilité de vos dépendances sur la page de suivi.
- Benchmarkez votre application sur les deux builds pour quantifier les compromis.
- Pour les workloads CPU-bound multithreadés, réécrivez tout parallélisme basé sur le multiprocessing pour utiliser le threading à la place, et mesurez l'amélioration.
| Type de workload | Build recommandé | Pourquoi |
|---|---|---|
| CPU-bound, multithreadé | Free-threaded (python3.14t) | Vrai parallélisme sur plusieurs cœurs |
| I/O-bound (web, APIs, BDD) | L'un ou l'autre (build GIL suffit) | GIL libéré pendant les I/O ; pas de bénéfice du free-threading |
| Monothread | Build GIL (python3.14) | ~10 % plus rapide ; le free-threading ajoute de l'overhead |
| Extensions C lourdes (non mises à jour) | Build GIL | Le free-threading peut provoquer des crashes si les extensions ne sont pas thread-safe |
| Entraînement/inférence ML | Free-threaded (si limité par l'orchestration) | Preprocessing Python parallèle ; code GPU inchangé |
Tableau 4 : choisir le bon build Python pour votre workload.
Le GIL n'est pas mort, mais vous pouvez désormais choisir de vous en passer
Le Global Interpreter Lock a été l'un des aspects les plus débattus de Python pendant plus de trente ans. C'était une décision de conception pragmatique qui a rendu Python plus simple et plus sûr, au prix du vrai parallélisme multi-cœur. Pendant des décennies, les développeurs l'ont contourné par le multiprocessing, les extensions C et des architectures inventives. Mais le monde a changé : les processeurs multi-cœurs sont partout, les jeux de données sont colossaux, et Python est la lingua franca du machine learning et de la data science.
Le build free-threaded de CPython, désormais officiellement supporté dans Python 3.14, représente un tournant fondamental. Il ne supprime pas le GIL purement et simplement car le build avec GIL reste le build par défaut et continuera d'être maintenu. Mais pour la première fois, vous avez un véritable choix. Vous pouvez exécuter Python sans le GIL, et votre code CPU-bound multithreadé exploitera réellement les cœurs. L'écosystème s'adapte rapidement, avec NumPy, Pandas, PyTorch et des centaines d'autres bibliothèques ajoutant leur support.
Il ne s'agit pas d'une révolution qui se fait du jour au lendemain. C'est une transition qui se déroulera sur plusieurs cycles de versions de Python. Mais la direction est claire, et les fondations sont solides. Si le GIL vous a frustré, 2026 est l'année pour commencer à expérimenter. Installez python3.14t, lancez vos tests, et découvrez ce que le vrai parallélisme Python ressenti. Le verrou est enfin optionnel, et l'avenir de Python est free-threaded.