2 puntos por GN⁺ 2024-12-13 | 1 comentarios | Compartir por WhatsApp
  • Con solo alrededor de 3000 bytes de código C construye un motor de inferencia de GPT-2 que procesa en un solo flujo desde la carga de pesos hasta la tokenización, la ejecución del Transformer y la conversión de salida
  • Aunque mantiene un tamaño de código pequeño, genera respuestas con GPT-2 Small en unos segundos en máquinas modernas gracias a KV caching, multiplicación de matrices rápida y paralelización OMP opcional
  • La calidad de salida está en un nivel “objetivamente bastante malo”, y siguen existiendo limitaciones prácticas como el manejo de UTF-8 y los requisitos de memoria al ejecutar modelos grandes
  • La implementación se divide en operaciones de matrices, capas neuronales, Transformer, Byte Pair Encoding, I/O y carga de pesos y BPE, mostrando la estructura completa de un pequeño motor de inferencia
  • GPT-2 es un modelo open source de 2019 mucho más débil que GPT-4, pero demuestra que las piezas clave para ejecutar modelos de lenguaje modernos pueden expresarse incluso en código C pequeño

Ejecutar GPT-2 en 3000 bytes de C

  • Este programa es una implementación de GPT-2 sin dependencias que lee las matrices de pesos y el archivo BPE desde los archivos originales de TensorFlow
  • La entrada se tokeniza con un codificador simple de Byte Pair Encoding (BPE), y la salida se vuelve a convertir en texto con un decodificador BPE
  • La estructura interna va desde un paquete básico de álgebra lineal hasta operaciones de matrices, la arquitectura Transformer y el código de inferencia
  • El código está publicado en GitHub
  • GPT-2 Small genera una respuesta en unos pocos segundos en una máquina moderna
    • Implementa KV caching
    • Usa multiplicación de matrices eficiente
    • Puede activar paralelización OMP de forma opcional

Requisitos de ejecución y limitaciones

  • Con esta implementación se puede crear un programa conversacional tipo ChatGPT, pero la calidad de salida no es buena
  • Hay algunas peculiaridades en el manejo de caracteres UTF-8
  • Ejecutar el modelo tamaño XL con un contexto largo puede requerir alrededor de 100 GB de RAM
  • Si se usa entrada ASCII junto con GPT-2 Small, puede ejecutarse casi en cualquier lugar

Cómo funcionan GPT-2 y Transformer

  • ChatGPT es una aplicación que permite conversar con un modelo de lenguaje como si fuera una persona, y GPT-4 se presenta como el modelo más reciente que impulsa ChatGPT
  • Este programa en C reproduce un comportamiento parecido al de ChatGPT usando GPT-2, un modelo de 2019
  • GPT-2 es un modelo de aprendizaje automático de la familia Transformer
  • Transformer recibe como entrada una secuencia de palabras de tamaño fijo y predice la siguiente palabra
  • Repitiendo el mismo procedimiento, puede generar secuencias de longitud arbitraria

Operaciones de matrices y compresión basada en macros

  • Como las redes neuronales están compuestas por operaciones de matrices, la implementación comienza con una estructura Matrix mínima
    • float* dat
    • int rows, cols
  • Las operaciones necesarias se dividen en dos grandes tipos
    • operaciones matriz-constante
    • operaciones matriz-matriz
  • Usa macros de C para reducir estructuras de bucles repetidas y generar varias funciones cambiando solo ciertos operadores
  • Como #define en C se parece más a una sustitución simple, se pueden pasar como argumentos del macro no solo operadores normales sino también expresiones con punto y coma para reducir el tamaño del código

Multiplicación de matrices rápida

  • La multiplicación básica de matrices parte de una implementación simple O(n³) con tres bucles anidados
  • Luego cambia los bucles para leer y escribir repetidamente la misma memoria, considerando las características de caché y acceso a memoria
  • La implementación rápida incrementa j y k de 4 en 4, y usa bucles internos k2 y j2
  • En la etapa de inferencia, agrega un método para multiplicar solo una parte de la matriz A por B, con el fin de reutilizar algunos resultados ya calculados

Implementación de capas neuronales

  • Para construir el Transformer, implementa directamente varias capas neuronales
  • La función de activación GELU está implementada con macros
  • Hay una función que procesa la parte triangular inferior de la matriz para la causal attention
    • Restringe la matriz de attention para que vea solo el pasado y no los tokens futuros
  • LayerNorm normaliza la media y la varianza de cada capa
  • La función Linear suma el bias después de la multiplicación de matrices usando un esquema de tiling

Núcleo del Transformer

  • La implementación del Transformer repite el siguiente flujo en cada capa
    • Calcula query, key y value de una sola vez pasando por LayerNorm y Linear
    • Divide qkv por head
    • Calcula el producto entre query y key y aplica el procesamiento de causal attention
    • Multiplica el resultado de softmax por la matriz value
    • Reúne los resultados y aplica la residual connection
    • Pasa por GELU y Linear, y vuelve a aplicar la residual connection
  • Al final, pasa por el LayerNorm final y luego multiplica la salida de la última posición de token por los pesos de embedding para calcular los candidatos del siguiente token

Cómo funciona el KV caching

  • En la inferencia de Transformer, después de generar un token no hace falta volver a calcular toda la función para producir el siguiente
  • Si se reutiliza la mayor parte de los resultados calculados hasta el token N, para generar el token N+1 solo se necesita trabajo adicional parcial
  • La implementación realiza todas las asignaciones de forma secuencial dentro del mismo bloque de memoria
  • Hace que cada multiplicación de matrices use siempre la misma memoria, de modo que en la siguiente iteración no se inicialice en cero y se conserven los resultados previos
  • En la nueva iteración, solo calcula la fila N+1

Implementación de Byte Pair Encoding

  • Como los modelos de lenguaje requieren entradas de tamaño fijo, es difícil manejar directamente palabras como unidades cuando hay una cantidad infinita de ellas
  • Un modelo a nivel de caracteres tendría que aprender desde cero el significado de cada palabra y además reduciría el tamaño de contexto efectivo en aproximadamente la longitud media de las palabras
  • Modelos como GPT-2 usan BPE para crear tokens a partir de fragmentos de palabras
    • Una palabra frecuente puede convertirse en un solo token
    • Una palabra poco frecuente se divide en fragmentos más pequeños
    • Por ejemplo, nicholas podría dividirse como nich, o, las
  • El algoritmo BPE típico fusiona repetidamente pares de tokens adyacentes
  • Esta implementación en C usa un enfoque recursivo que potencialmente puede tardar tiempo exponencial en vez de un algoritmo lineal, para reducir el tamaño del código
    • Busca entradas del vocabulary que coincidan con el prefijo de la palabra actual
    • Tokeniza recursivamente la cadena restante
    • Elige la mejor tokenización según la longitud y el índice en el vocabulary

Carga de pesos

  • Los pesos de la red neuronal deben leerse desde disco, y el archivo es una serialización binaria plana de floats de 32 bits
  • Los tamaños de modelo de GPT-2 usan la misma arquitectura y guardan los pesos en el mismo orden, así que basta con leer secuencialmente matrices con la forma correcta
  • El orden de almacenamiento de las capas no coincide con lo esperado
    • Después de las capas 0 y 1 viene la 10
    • Porque los nombres están ordenados en orden lexicográfico
    • En el ordenamiento de cadenas, 10 va antes que 2
  • La implementación usa código de permutación para convertir ese orden en el orden real de las capas

Carga del vocabulary BPE

  • Para ejecutar BPE, primero hay que leer desde disco el archivo de vocabulary
  • El archivo original tiene un formato pensado para leerse desde Python y no está hecho para ser fácil de parsear con un código C pequeño
  • El archivo no es una lista de palabras sino una lista de fusiones BPE
    • Por ejemplo, en vez de almacenar directamente el token Hello, almacena que hay que fusionar H y ello
  • El archivo usa una codificación parecida a UTF-8, pero no exactamente igual
    • Los caracteres ASCII imprimibles se almacenan tal cual
    • Los caracteres no imprimibles en el rango 0~31 se codifican como 188 + valor del carácter
    • Por ejemplo, el espacio se codifica como el token Ġ
  • Como Ġ en disco es 0xc4 0xa0 en UTF-8, se necesita un procesamiento adicional para volver a convertirlo en un espacio

Lo que muestra este código pequeño

  • Décadas de avances en machine learning pueden comprimirse en unos pocos miles de bytes de código
  • Exceptuando los pesos reales del modelo, casi no falta ninguno de los elementos necesarios para ejecutar una red neuronal moderna
  • Esta implementación se hizo principalmente por diversión, pero sirve como ejemplo de que una red neuronal en realidad puede ejecutarse con componentes simples

1 comentarios

 
GN⁺ 2024-12-13
Opiniones de Hacker News
  • No ejecuté el código personalmente, pero me impresiona que sea tan pequeño.
    Si pensamos que los primeros programas ELIZA eran más grandes, en los últimos 4 años llegamos a poder meter algo así a nivel de bytes.
    Si alguien sabe dónde está escondida la magia, me gustaría que lo explicara. Me pregunto si es la función GELU o el modelo que se descarga con el script de Bash.

    • La mayor parte de la magia está en el archivo de modelo de 475 MB que se descarga con el script de Bash.
    • Lo ejecuté y no me pareció muy impresionante.
      A Who are you? responde I am Alice., y si le preguntas por computadoras o funciones repite I am a computer model trained by OpenAI. How can I help you?.
      Si le pides que explique una suma, da una explicación de multiplicación, y con 2+2 o Sum 2+2 apenas lo repite tal cual.
  • Recuerdo haber trasteado con GPT-2 cuando salió por primera vez.
    Exporté registros de chat con un amigo, ajusté GPT-2 con fine-tuning y lo hice imitar las conversaciones entre nosotros; era muy gracioso y a veces inquietantemente preciso.
    Me pregunto a qué se debió el salto enorme de GPT-2 a GPT-3. No sé si fue un modelo más grande, más datos, o ambas cosas.
    Sé que RLHF marcó una gran diferencia, pero incluso el modelo base de GPT-3, si le dabas suficientes ejemplos, era bastante útil solo como completador de texto.

  • No estoy muy seguro, pero hay cuentos de hadas que me gustan y que fueron escritos por GPT-2.
    https://deepdreams.stavros.io/episodes/the-princess-the-fair...

    • Son historias realmente buenas, de verdad entretenidas, y también sirven para quedarse dormido escuchándolas.
      Me pregunto si se hicieron con el GPT-2 de esta página.
    • Es impresionante y raro, pero más o menos 90% coherente, lo que crea esa atmósfera peculiarmente extraña.
  • Me pareció divertida la parte que dice: “Esto es mayormente por diversión, pero es un buen ejemplo de lo simples que son realmente las redes neuronales”.
    Shhh, no se lo digamos a nadie. La inteligencia artificial es magia negra que se usa para ganar dinero.

  • ¿GPT-2 está ajustado para instrucciones como para poder usarse en un chat real?
    Si no, me parece bastante forzado llamarlo un clon de ChatGPT.

    • El artículo ya dice esto: que si no te importa la calidad de salida, puedes crear algo tipo ChatGPT, y que objetivamente la salida es bastante terrible, pero funciona.
      En la práctica es inutilizable y casi no tiene relación más allá de colgarse del nombre. Aun así, es un programa que compila y corre.
      Al ver reacciones que elogian el rendimiento de un proyecto cuyo propio autor admite que no funciona bien, parece que al final lo importante es llamar la atención con palabras de moda.
  • La frase “¿Están viendo, lenguajes con macros de verdad? ¡Lisp no siempre es mejor que C!” esta vez es aceptable. Es un chiste hacia arriba.
    Si no viste el enlace al código, está escondido en el texto: https://github.com/carlini/c-chat-gpt-2

  • He visto chatbots clásicos de inteligencia artificial mejores.
    https://www.cs.cmu.edu/afs/cs/project/ai-repository/ai/areas...
    Splotch compila bien en sistemas Unix modernos con apenas unos pequeños arreglos.

  • Me pregunto si alguien lo ejecutó localmente para ver qué tipo de salida produce este GPT-2.

    • Da la impresión de que casi siempre repite la misma salida.
      Aun así es bastante interesante, y me dan ganas de meterme a revisarlo y ajustarlo. Hace tiempo que quería trastear con GPT-2 en local.
    • Por lo que leí, si se usan la misma temperatura y semilla, el modelo GPT-2 cargado normalmente y el modelo cargado en este programa deberían producir exactamente la misma salida.
      No pude verificar directamente la temperatura y la semilla en el código; estaba más que nada intentando ver por qué lo habían ofuscado.
      Aunque se desofusque, el código no debería quedar gigantesco; si ronda los 10 mil caracteres, creo que verlo en pantalla seguiría siendo bastante impresionante.
  • Hoy en día, con gptscript puedes implementar rápidamente tu propio ChatGPT.
    https://github.com/gptscript-ai/gptscript

  • GELU sí parece magia:
    UNARY(GELU, b / 2 * (1 + tanh(.7978845 * (b + .044715 * b * b * b))))

    • Esto es solo una aproximación práctica a la definición matemática real de GELU.
      La definición es GELU(x) := x * Φ(x), donde Φ(x) es la función de distribución acumulada de la distribución gaussiana.
    • Tiene una pinta que recuerda a la raíz cuadrada inversa rápida.