1 puntos por GN⁺ 2023-07-30 | 1 comentarios | Compartir por WhatsApp
  • El Python Steering Council busca aceptar la PEP 703 para hacer que el GIL sea opcional en CPython, y la reacción de la comunidad tanto a la propuesta no-GIL como a la PEP 703 ha sido en general positiva
  • El objetivo a largo plazo es unificar todo en una compilación no-GIL, y evitar una situación en la que las compilaciones with-GIL y no-GIL, así como el ecosistema de módulos de extensión, queden divididos de forma permanente
  • Durante la transición, la compatibilidad hacia atrás es la restricción clave, y el código de terceros modificado para soportar no-GIL debe seguir funcionando también en compilaciones with-GIL
  • El plan es una transición en 3 etapas: compilación experimental a corto plazo, compilación soportada a mediano plazo y compilación predeterminada a largo plazo; la compilación experimental podría entrar en Python 3.13, pero que se retrase a 3.14 no se considera un problema
  • Antes de cambiar la compilación predeterminada, debe comprobarse el soporte de la comunidad y la experiencia con APIs, empaquetado y distribución; si la confusión supera los beneficios, habrá que abandonar la PEP 703 y buscar otra solución

Dirección del Steering Council sobre la aceptación de la PEP 703

  • El Python Steering Council tiene la intención de aceptar la PEP 703
  • En la encuesta sobre la propuesta no-GIL hubo una reacción generalmente positiva, y el Steering Council también ve con buenos ojos tanto la idea general como la PEP 703
  • Sin embargo, los detalles de la aceptación todavía se están trabajando y se espera finalizarlos en las próximas semanas

Principios de transición para evitar una bifurcación permanente

  • A largo plazo, probablemente en un periodo de más de 5 años, la compilación no-GIL debería convertirse en la única compilación
    • No quieren una situación en la que las compilaciones with-GIL y no-GIL queden separadas de manera permanente
    • También buscan evitar que el ecosistema de módulos de extensión quede dividido de forma permanente entre ambas compilaciones
  • La compatibilidad hacia atrás debe manejarse con mucho cuidado
    • No quieren crear otra situación de transición como la de Python 3
    • Aunque el código de terceros cambie para adaptarse a la compilación no-GIL, esos cambios también deben funcionar en compilaciones with-GIL
    • Los problemas de compatibilidad con versiones anteriores de Python deben tratarse por separado
    • Esto no es Python 4
  • Los requisitos de compatibilidad ABI, los detalles entre ambas compilaciones y el impacto en la compatibilidad hacia atrás todavía están en revisión

Condiciones que deben verificarse antes de cambiar el valor predeterminado

  • Antes de convertir la compilación no-GIL en la opción predeterminada, hay que comprobar si existe suficiente apoyo de la comunidad
  • No basta con cambiar el valor predeterminado y dejar que la comunidad descubra por su cuenta el trabajo necesario
  • Los desarrolladores del core deben experimentar directamente con el nuevo modo de compilación y con todo lo que lo rodea
    • Al ordenar la seguridad de hilos del código existente, podrían hacer falta nuevas C API y Python API
    • Las conclusiones obtenidas en ese proceso deben compartirse con la comunidad de Python
    • Debe verificarse que los cambios que desean los desarrolladores del core y los que se pedirán a la comunidad sean razonables
  • Hasta que no-GIL se convierta en el valor predeterminado, debe mantenerse la posibilidad de cambiar de rumbo si se concluye que el cambio genera demasiada confusión y pocos beneficios
    • En ese caso, también debe ser posible decidir revertir todo el trabajo realizado
    • Por eso, el código exclusivo para no-GIL debe ser identificable hasta cierto punto

Plan de transición en 3 etapas

  • A corto plazo, se añadirá la compilación no-GIL como un modo de compilación experimental
    • Podría entrar en Python 3.13
    • Si se retrasa hasta Python 3.14, no se considera un problema
    • El estado experimental busca dejar claro que los desarrolladores del core apoyan este modo de compilación, pero que no puede esperarse que la comunidad lo soporte de inmediato
    • Hará falta tiempo para habilitar el soporte comunitario en términos de diseño de APIs, empaquetado y distribución
    • No se recomienda que las distribuciones ofrezcan una compilación experimental no-GIL como intérprete predeterminado
  • A mediano plazo, la compilación no-GIL pasará a ser una compilación soportada, pero todavía no será la predeterminada
    • Hace falta confianza en que existe suficiente soporte de la comunidad como para permitir su uso en producción
    • En esta etapa se fijará una fecha objetivo o una versión de Python para convertir no-GIL en el valor predeterminado
    • El momento dependerá en gran medida de la compatibilidad hacia atrás de los cambios en la API, del manejo de la stable ABI y de la cantidad de trabajo que la comunidad considere necesario
    • Podría tomar al menos 1 a 2 años, o incluso más
    • Una vez declarada como soportada, algunos distribuidores podrían empezar a ofrecer no-GIL como opción predeterminada, aunque eso dependerá de cuántos paquetes de Python soporten no-GIL en ese momento
  • A largo plazo, se busca convertir no-GIL en la opción predeterminada y eliminar los rastros del GIL
    • No se romperá la compatibilidad hacia atrás innecesariamente
    • Mantener durante mucho tiempo dos modos de compilación comunes puede aumentar la carga para la comunidad, por ejemplo al duplicar los recursos de prueba y los escenarios de depuración
    • Aun así, tampoco pueden apresurarse, y esta etapa podría tardar hasta 5 años

Reevaluación continua y posibilidad de detener el proceso

  • Durante todo el proceso, no solo el Steering Council sino también los desarrolladores del core deberán reevaluar continuamente el progreso y el calendario propuesto
  • No quieren que este trabajo se convierta en otra lucha de 10 años por la compatibilidad hacia atrás
  • Si aparecen señales de que la PEP 703 se está volviendo problemática, debe poder detenerse y buscar otra solución
  • Habrá que revisar periódicamente si el trabajo en curso realmente vale la pena

1 comentarios

 
GN⁺ 2023-07-30
Comentarios de Hacker News
  • Durante décadas, mucho código de bibliotecas en C venía con advertencias en los manuales sobre su inestabilidad en contextos asíncronos, reentrantes o recursivos.
    Aun así aprendimos a lidiar con eso, y se fueron desplegando gradualmente versiones reentrantes y seguras sin volver demasiado inestables las API.
    Había cosas como parseo de cadenas que tokenizaba in situ, llamadas DNS que usaban búferes estáticos y código que dependía del comportamiento particular de la pila en VAX; creo que el GIL fue tanto una bendición como una maldición.

    • Recuerdo revisar minuciosamente la documentación del runtime de C para encontrar funciones que no fueran reentrantes.
      Gracias a eso, creo que desarrollé el hábito de revisar la documentación incluso de API que conozco más o menos, por si había pasado por alto algún detalle importante o algo había cambiado.
      En esa época hacía C++ multiplataforma, y cuando vi Java por primera vez tenía concurrencia, recolección de basura y varias funciones más fáciles de usar que C++ desde el inicio; estaba convencido de que iba a despegar mucho.
      Después, los desarrolladores mainstream empezaron a usar Python; por lo que recuerdo, originalmente era un lenguaje de extensión embebible y simple, así que el GIL tenía más sentido.
    • El modo sin GIL es opcional, y las bibliotecas se marcarán como “compatibles con no-GIL”; el ecosistema irá soportándolo cada vez más de forma gradual.
      No es que alguien vaya a activar un interruptor y romper de golpe montones de código C sospechoso.
    • Ese estado no puede mantenerse para siempre.
      Ahora ya existen CPU de 128 núcleos, y estamos en una época en la que incluso las CPU económicas tienen 6 núcleos, así que la limitación de quedar atados al rendimiento de un solo núcleo será cada vez mayor con el tiempo.
    • Cuando empecé en serio con C en los 90 también veía estas advertencias.
      Al principio creía que era un problema que se resolvería en unas semanas, como mucho en unos meses; era evidente que no sabía nada.
    • Me cuesta ver una equivalencia aquí.
      En C, la interacción con funciones que no son thread-safe es mucho más directa, y cuando uno usa C normalmente tiene más cuidado.
      En Python hay módulos C enteros con estado global, y después de cargar unos 10, sumando la complejidad del intérprete, pronto nadie sabe qué está pasando.
      Incluso ahora, la mayoría de los desarrolladores —incluidos los desarrolladores core— ni siquiera revisan fugas de memoria; no creo que vayan a ejecutar tsan, y aunque lo hagan probablemente será con una pequeña batería de pruebas que cubra solo el 10% del código.
      Viendo las prácticas de desarrollo de software en Python, especialmente en el área de IA, soy muy pesimista con esta función.
  • Es interesante.
    Python está compuesto en gran parte por bibliotecas compartidas en C escritas sabiendo que podían depender de un bloqueo global.
    Algunas son lo suficientemente simples como para funcionar bien sin locks, pero otras todavía necesitarán locks y ahora estarán bajo presión para ejecutarse sin el GIL.
    Algunas de ellas implementarán sus propios locks dentro de su ámbito, y quizá lo que realmente le faltaba a Python eran llamadas improvisadas a mutex dispersas por todo el ecosistema.
    No esperaba que Python se fuera a romper introduciendo condiciones de carrera y deadlocks con el pretexto del rendimiento.
    Convertir en thread-safe una biblioteca C escrita bajo el supuesto de un bloqueo global es el tipo de trabajo que incluso un experto en concurrencia desaconsejaría y en el que es fácil equivocarse durante la implementación.
    Mi hipótesis es que la mayoría de quienes han escrito extensiones C para Python no son expertos en concurrencia, sino buenos programadores que no rehúyen los retos; con esa combinación, las condiciones de carrera/cuelgues/segfaults parecen casi inevitables.

    • Hacerlo como un opt-out explícito, a modo de primer paso, me parece bien.
      Pero esperar cambiarlo a opt-in dentro de 5 años es demasiado optimista.
      Todos los desarrolladores de bibliotecas tendrían que arreglar sus propias bibliotecas, e incluso bibliotecas de Python; es un trabajo difícil y, aunque se haga bien, nadie lo nota, así que es complicado que sea recompensado.
      Muchas bibliotecas ni siquiera tenían casos de uso de multiprocesamiento, y en las bibliotecas grandes aparecerán inevitablemente bugs sutiles, así que las quejas imposibles de reproducir y el abandono por parte de los desarrolladores parecen un resultado garantizado.
    • La mayoría de las bibliotecas C que se llaman con frecuencia desde Python también tienen bindings para otros lenguajes, así que pensaría que a estas alturas ya deberían ser thread-safe.
      Python con GIL también soporta threads, así que esas bibliotecas al menos deberían ser reentrantes.
      Si una biblioteca es reentrante pero no thread-safe, podría bastar con agregar un único lock global que envuelva todas las llamadas, lo cual se parece bastante a lo que hacía el GIL.
      Hacer que las bibliotecas existentes funcionen sin el GIL parece relativamente directo en muchos casos, aunque se pueda sacrificar el paralelismo.
      El problema central probablemente sean las bibliotecas que hacen callbacks desde el lado de C hacia el runtime de Python.
  • Es una pregunta ingenua, pero con los paquetes asyncio y multiprocessing, me pregunto quién necesita No-GIL.
    Nunca he tenido problemas por el GIL en Python; siempre lo he esquivado levantando un ThreadPool o un ProcessPool, o usando una biblioteca asíncrona cuando hacía falta.
    Me pregunto si hay casos de uso de No-GIL que no se resuelvan con multiprocessing.
    Pensaba que la ejecución de un solo hilo, sin el overhead de las primitivas de concurrencia, era lo mejor para la computación de alto rendimiento. Como mostró LMAX Disruptor.

    • El punto central es únicamente el rendimiento.
      asyncio es esencialmente de un solo hilo, así que usa un solo núcleo; multiprocessing usa múltiples núcleos, por lo que es mejor para el rendimiento, pero cada proceso es relativamente pesado y se suma el overhead de la memoria compartida.
      El multithreading basado en GIL usa un solo núcleo y además es difícil de usar correctamente.
      El multithreading sin GIL usa múltiples núcleos, pero es difícil de usar; no conozco bien la implementación, pero la memoria compartida debería ser más rápida que con multiprocessing.
      Si estuviera diseñando un sistema nuevo, estoy de acuerdo en que, para casi todos los casos de uso de Python, conviene no tocar los hilos y usar asyncio/multiprocessing.
      Muchos programas en Python que necesitan multithreading rápido probablemente no deberían haberse escrito en Python desde el principio, pero como ya hay gente que escribió código intensivo en CPU en Python, No-GIL es práctico.
    • Hay muchos casos en los que No-GIL no se resuelve con multiprocessing.
      Si tienes un servidor web que responde simultáneamente a varios clientes usando estado compartido, multiprocessing usa pickle para enviar y recibir datos, lo que implica un gran overhead de rendimiento.
      Por ejemplo, si quieres tener una estructura de datos de 1 GB en memoria y hacer cálculos en paralelo, es difícil lograr buen rendimiento con multiprocessing.
      Con pickle no se pueden compartir todos los objetos, y los errores por objetos no serializables con pickle son muy difíciles de depurar en estructuras de datos complejas.
      En particular, los objetos creados por bibliotecas nativas pueden no ser compartibles.
      Los procesos que necesitan compartir estado en ejecución también son muy difíciles con el módulo multiprocessing, e incluso el exporter de Prometheus para Flask necesita un hack raro usando un directorio temporal para reunir estadísticas de todos los procesos.
    • En PEP 703, Manuel Kroiss, del equipo de aprendizaje por refuerzo de DeepMind, explica que por el cuello de botella del GIL terminan reescribiendo la base de código de Python en C++, lo que la vuelve menos accesible para los investigadores.
      Dice que en muchas aplicaciones de DeepMind quisieran ejecutar unas 50 a 100 hebras por proceso, pero que a menudo el GIL se vuelve un cuello de botella incluso con menos de 10.
      Como workaround a veces usan subprocesos, pero en muchos casos el overhead de comunicación entre procesos se vuelve demasiado grande, y al final terminan trasladando grandes partes de la base de código de Python a C++.
      En usos promedio como aplicaciones web, multiprocessing puede ser suficiente, pero en cargas de trabajo de IA a gran escala como las de Google y DeepMind, el GIL realmente limita el uso de Python.
      Esta es también la razón por la que Meta quiere invertir tres años de ingeniería en este trabajo: https://news.ycombinator.com/item?id=36643670
    • Para problemas CPU-bound, asyncio es completamente inútil porque el event loop sigue corriendo solo en un núcleo.
      Como su nombre lo indica, realmente solo ayuda en problemas I/O-bound.
      Compartir datos entre varios procesos es un dolor enorme, y combinar el control de datos con la orquestación de procesos es un dolor aún mayor.
      Los procesos son costosos y, por la dificultad de compartir datos mencionada antes, greenlet tampoco llega a ser una alternativa real.
    • No parece que vaya a ser tan revolucionario para una aplicación web promedio.
      Pero en áreas donde Python tiene mucho peso, como IA y ciencia de datos, poder levantar y ejecutar muchos hilos CPU/GPU-bound es una gran ventaja.
  • Como muchas extensiones en C no fueron escritas pensando en multithreading, pueden surgir problemas, y de hecho parece probable que ocurra.
    Aquí hay un pequeño ejemplo inseguro si lst puede ser accedido desde otro hilo: https://news.ycombinator.com/item?id=36649769
    Incluso ahora, si el código en C hace un callback a bytecode de Python mediante el método __del__ y ese bytecode es lo bastante largo, probablemente alrededor de 100 instrucciones, puede producirse un cambio de contexto.
    Sin embargo, es un caso extremadamente raro, y mucho código de extensiones en C no está escrito teniendo en cuenta esa situación.
    Quienes usan extensiones en C podrían estar dependiendo de que se ejecuten de forma atómica.
    Por ejemplo, pasar arrays de numpy de ida y vuelta en un pool de hilos funciona bien ahora, pero sin el GIL podría romperse.

    • Seguramente se encontrarán muchísimos problemas así y, por desgracia, aparecerán como condiciones de carrera difíciles de encontrar y depurar.
      Por eso la propuesta y la dirección del trabajo apuntan a que el modo sin GIL sea completamente opcional y no el valor predeterminado.
      La minoría valiente que lo active debe estar dispuesta a dedicar muchísimo tiempo a encontrar y corregir condiciones de carrera sutiles en décadas de código de bibliotecas de Python.
      Los early adopters sufrirán bastante o, más probablemente, limitarán el uso de non-GIL a procesos dedicados muy especializados con la menor cantidad posible de dependencias.
    • Me parece bien.
      Es pasar de sospechar que habrá problemas a saber exactamente dónde están, y luego solo queda ir reduciendo esa lista uno por uno.
      Se puede agregar alguna forma de mutex alrededor del código o cambiar a una implementación alternativa que no sea código nativo y tenga menos probabilidades de causar problemas.
      El argumento en contra parece ser sobre todo que hay mucho trabajo, no que sea imposible.
      Si hay suficiente gente para hacerlo, puede haber resultados.
    • “Extremadamente raro”, desde la perspectiva de alguien que trabaja profesionalmente con runtimes paralelos/asíncronos que soportan miles de servidores funcionando continuamente, significa que en la práctica se rompe siempre, pero depurarlo es imposible.
    • Creo que los desarrolladores principales de CPython conocen muy bien estos problemas.
      Si no fuera así, habrían anunciado un plan para eliminar de golpe todo el GIL, en vez de un enfoque gradual que permite a la gente optar explícitamente por el modo No-GIL.
    • Quizá sea un buen momento para crear extensiones en C con concurrencia como prioridad que puedan competir con las extensiones existentes.
  • Basta recordar la transición de texto a Unicode, de 32 bits a 64 bits, de Intel a ARM, y el Y2K.
    No-GIL es un cambio mucho menor y puede seguir el mismo camino de transición sin romper las cosas de forma radical.
    Incluso si algo se rompe, habrá una forma bien definida de manejar esos casos.
    Sobrevivimos a esas transiciones de todos modos, y da gusto ver que se avance.
    Abrirá más áreas que hasta ahora se marcaban como imposibles.
    Una de las cosas que Swift hizo bien al principio fue poner los cambios incompatibles dentro de una promesa, y todos supieron dónde estaban parados y se adaptaron bien.
    A veces quisiera que Python tomara el mismo camino.

    • Eso es un poco distinto.
      En el paso de 32 bits a 64 bits, a ARM, y en el Y2K, se podía probar si funcionaba o no.
      Claro que las pruebas podían no cubrir los casos de falla, pero las pruebas realizadas eran deterministas.
      Pero aquí, por más pruebas que hagas, la respuesta es “está bien, o todavía no tocaste la condición de carrera correcta”.
    • No me preocupa la dificultad de la migración, sino que el estado final pueda ser realmente peor que el actual.
    • Pasar de texto a Unicode fue un problema especialmente grande para Python.
  • Aunque se dice explícitamente que no se quiere repetir el escenario de transición de Python 3, el enfoque actual se ve inquietantemente cercano a ese camino.
    Mucho depende de la comunidad de Python y de los canales de distribución.
    La comunidad podría no adoptarlo a tiempo, o distribuciones como Ubuntu, Fedora y Anaconda podrían adelantarse demasiado pronto.
    Es demasiado pronto para afirmarlo, pero me pregunto cuánto control real tiene el Steering Council para evitar este tipo de escenario.

    • Dijeron que, cinco años después de que No-GIL esté disponible, quieren que sea el único modo de compilación, pero eso es a la vez demasiado largo y demasiado corto.
      Cinco años es demasiado tiempo para que Python tenga dos modos.
      Media década basta para que ambos modos se consoliden como el statu quo, y aun después seguirán existiendo viejas publicaciones de Stack Overflow.
      No soy optimista en que cinco años no se conviertan en diez años de incertidumbre y roturas.
      Por el contrario, cinco años también podrían ser demasiado poco para sacar todo el código en C, arreglarlo, probarlo y decir que está maduro.
    • Se parecerá a la situación de 2to3.
      Empresas que prometieron soporte podrían convertir mecánicamente algunos proyectos y molestar a los desarrolladores reales, o amenazarlos con hacer forks.
      Los bugs los terminarán puliendo desarrolladores reales no remunerados durante años.
      Pero Python necesita algún “éxito”, y esto sirve como buen mensaje publicitario.
      En el mundo de Python, la corrección no parece importar demasiado.
    • En el paso de 2 a 3 se gastó, sin demasiada razón, la carta de una transición de versión mayor que rompe compatibilidad, y ahora, ante un cambio grande, se dice que “no será como el paso de 2 a 3”.
      Por esa historia, suena a que prefieren mantener dos modos de ejecución dentro de CPython 3 en vez de avanzar hacia otra transición mayor.
  • Es bueno que sean muy conscientes de que esto fácilmente podría convertirse en un desastre de Python 4.
    Hay que tener extremo cuidado de no afectar accidentalmente el comportamiento con GIL.
    Si alguna forma de GIL emulado no es exactamente igual al GIL real, pueden aparecer todo tipo de casos extraños.

    • Hasta ahora solo vi dos razones por las que sería distinto de 2 -> 3.
      Primero, no quieren que pase eso.
      Segundo, si pasa, se rendirán rápido.
      Ambas son importantes, pero parece faltar la tercera pieza clave: “y así es como vamos a lograrlo”.
    • Estoy seguro de que la intención es buena, pero no estoy seguro de que puedan evitarlo.
      Ya están diciendo que GIL y No-GIL podrían coexistir durante más de 5 años.
      Para quienes crean herramientas, eso significa que el costo se duplica al menos durante los próximos 5 años.
      Porque, ya sea en producción o de forma experimental, la gente querrá usar herramientas en ambos modos.
  • GIL significa Global Interpreter Lock.
    Hay una buena explicación aquí: https://realpython.com/python-gil/

  • Aquí hay dos grandes problemas.
    Primero, algunas mejoras valen la pena aunque rompan compatibilidad hacia atrás, y eliminar el GIL es una de ellas.
    Es discutible si los cambios de Python 3 valían la pena, y que print se volviera una función no parece especialmente valioso.
    Aun así, la transición de 2 a 3 fue exagerada por una minoría ruidosa, y por mi experiencia migrando más de 5 bases de código de 2 a 3, en la mayoría hubo pocos problemas.
    El mayor problema fue una base de código en la que desarrolladores anteriores habían metido librerías para todo, convirtiéndola en un montón de librerías abandonadas; ese tipo de código tendría problemas aunque el lenguaje central no rompiera compatibilidad.
    La respuesta no es presionar para que el lenguaje nunca pueda romper compatibilidad, sino no esperar que traer todo pip sea una estrategia sostenible.
    Ahora el Steering Council recibió tantas críticas de una minoría ruidosa que quedó con miedo de introducir cambios incompatibles.
    Pero eliminar el GIL es una parte demasiado fundamental de cómo funciona Python, así que debería ser un cambio incompatible; es mejor reconocerlo y planear la transición que intentar lo imposible, por miedo a los usuarios, de hacerlo sin romper nada y sin admitir la realidad.
    Incluso en Python 3.11 mi base de código se rompió, y no fue difícil arreglarla, pero me habría gustado una mejor comunicación de que esto podía pasar.
    Segundo, el problema más fundamental es que muchas otras funciones de Python fueron construidas alrededor del GIL.
    En particular, el paradigma asíncrono tiene mucho sentido por el GIL, pero si no hubiera GIL, en retrospectiva un modelo de actores tipo Erlang con send/recv habría sido una dirección mucho mejor.
    Revertir eso es difícil, y parece demasiado poco y demasiado tarde, como si Python estuviera siendo empujado hacia un conjunto menos cohesivo de funciones que no encajan bien entre sí.

  • Agradezco a los desarrolladores principales de Python y al Steering Council; Python es uno de mis lenguajes favoritos junto con Java y C.
    Recibo con mucho entusiasmo el multithreading real en Python.
    Según el proyecto, uso tanto multiprocessing como multithreading; hay un ejemplo de multiprocessing en [0], y un ejemplo de uso de Python Threads para tareas con mucha entrada/salida en [1].
    Pero usar hilos reales sería mucho más eficiente.
    Los hilos pueden enviarse y recibirse cantidades arbitrarias de datos como una única operación atómica y casi instantánea, algo que no es posible con una interfaz local de loopback, multiprocessing o pipes.
    Estoy trabajando en una arquitectura multihilo que llamo “three tier multithreading architecture”.
    https://github.com/samsquire/three-tier-multithreaded-archit...
    El objetivo es un servidor extremadamente escalable y de alto rendimiento, aunque probablemente Python no sea la herramienta adecuada para ese trabajo.
    [0]: https://news.ycombinator.com/item?id=36897054 explicación del uso de multiprocessing
    [1]: https://devops-pipeline.com/ ejemplo de uso de multithreading