2 points par GN⁺ 2024-12-13 | 1 commentaires | Partager sur WhatsApp
  • Avec seulement environ 3 000 octets de code C, il construit un moteur d’inférence GPT-2 qui traite tout le flux, du chargement des poids à la tokenisation, l’exécution du Transformer et la conversion de la sortie
  • Tout en conservant une taille de code réduite, il génère des réponses avec GPT-2 Small en quelques secondes sur une machine récente grâce au cache KV, à une multiplication matricielle rapide et à une parallélisation OMP optionnelle
  • La qualité de sortie est « objectivement assez mauvaise », et des contraintes pratiques subsistent, comme la gestion de l’UTF-8 et les besoins mémoire lors de l’exécution de grands modèles
  • L’implémentation est divisée entre opérations matricielles, couches de réseau neuronal, Transformer, Byte Pair Encoding, I/O, chargement des poids et du BPE, ce qui montre toute la structure d’un petit moteur d’inférence
  • GPT-2 est un modèle open source de 2019, bien moins puissant que GPT-4, mais les composants essentiels à l’exécution des modèles de langage modernes peuvent être exprimés même dans un petit code C

Un moteur d’exécution GPT-2 en 3 000 octets de C

  • Ce programme est une implémentation de GPT-2 sans dépendances, qui lit les matrices de poids et les fichiers BPE depuis les fichiers TensorFlow d’origine
  • L’entrée est tokenisée avec un encodeur Byte Pair Encoding (BPE) simple, et la sortie est reconvertie en chaîne de caractères avec un décodeur BPE
  • Sa structure interne enchaîne un paquet minimal d’algèbre linéaire, des opérations matricielles, l’architecture Transformer et le code d’inférence
  • Le code est publié sur GitHub
  • GPT-2 Small génère une réponse en quelques secondes environ sur une machine récente
    • Il implémente le cache KV
    • Il utilise une multiplication matricielle efficace
    • Il permet d’activer optionnellement la parallélisation OMP

Conditions d’exécution et limites

  • Cette implémentation permet de créer un programme conversationnel de type ChatGPT, mais la qualité de sortie n’est pas bonne
  • La gestion des caractères UTF-8 présente quelques particularités
  • L’exécution d’un modèle de taille XL avec une longue longueur de contexte peut nécessiter environ 100 Go de RAM
  • Avec une entrée ASCII et GPT-2 Small, il peut tourner presque partout

GPT-2 et fonctionnement des Transformers

  • ChatGPT est une application permettant de dialoguer avec un modèle de langage comme avec une personne, et GPT-4 est présenté comme le modèle récent qui alimente ChatGPT
  • Ce programme C implémente un comportement similaire à ChatGPT avec GPT-2, un modèle de 2019
  • GPT-2 est un modèle de machine learning de la famille des Transformers
  • Un Transformer prend en entrée une séquence de mots de taille fixe et prédit le mot suivant
  • En répétant la même procédure, on peut générer une séquence de longueur arbitraire

Opérations matricielles et compression par macros

  • Comme les réseaux neuronaux sont composés d’opérations matricielles, l’implémentation commence par une structure Matrix minimale
    • float* dat
    • int rows, cols
  • Les opérations nécessaires se répartissent en deux grandes catégories
    • Opérations matrice-constante
    • Opérations matrice-matrice
  • Des macros C réduisent les structures de boucles répétitives et génèrent plusieurs fonctions en ne changeant que certains opérateurs
  • Le #define du C étant proche d’une simple substitution, on peut passer en argument de macro non seulement des opérateurs ordinaires, mais aussi des expressions incluant des points-virgules, afin de réduire la taille du code

Multiplication matricielle rapide

  • La multiplication matricielle de base part d’une implémentation simple en O(n³) avec trois boucles imbriquées
  • Les boucles sont modifiées en tenant compte du cache et des caractéristiques d’accès mémoire, afin de lire et d’écrire de façon répétée dans les mêmes zones mémoire
  • L’implémentation rapide incrémente j et k par pas de 4, et utilise des boucles k2 et j2 en interne
  • Lors de l’inférence, une méthode supplémentaire multiplie seulement une partie de la matrice A par B afin de réutiliser une partie des résultats déjà calculés

Implémentation des couches du réseau neuronal

  • Plusieurs couches de réseau neuronal sont implémentées directement pour construire le Transformer
  • La fonction d’activation GELU est implémentée avec une macro
  • Une fonction traite la partie triangulaire inférieure de la matrice pour la causal attention
    • Elle limite la matrice d’attention afin de ne regarder que le passé, sans voir les tokens futurs
  • LayerNorm normalise la moyenne et la variance de chaque couche
  • La fonction Linear ajoute le biais par tuilage après la multiplication matricielle

Le cœur du Transformer

  • L’implémentation du Transformer répète le flux suivant pour chaque couche
    • Calculer query, key, value en une seule fois via LayerNorm et Linear
    • Diviser qkv par head
    • Calculer le produit de query et key, puis appliquer le traitement de causal attention
    • Multiplier le résultat du softmax par la matrice value
    • Regrouper les résultats et appliquer une residual connection
    • Passer par GELU et Linear, puis appliquer à nouveau une residual connection
  • À la fin, après une LayerNorm finale, la sortie à la position du dernier token est multipliée par les poids d’embedding pour calculer les candidats au token suivant

Méthode de cache KV

  • Dans l’inférence Transformer, après avoir généré un token, il n’est pas nécessaire de recalculer toute la fonction pour produire le token suivant
  • En réutilisant la majeure partie des résultats calculés jusqu’au N-ième token, seule une partie de travail supplémentaire est nécessaire pour générer le token N+1
  • L’implémentation effectue toutes les allocations séquentiellement dans le même bloc mémoire
  • Chaque multiplication matricielle est conçue pour toujours utiliser la même mémoire, afin de conserver les résultats précédents sans réinitialiser la mémoire à zéro à l’itération suivante
  • Lors de la nouvelle itération, seule la ligne N+1 est calculée

Implémentation du Byte Pair Encoding

  • Comme les modèles de langage nécessitent une entrée de taille fixe, il est difficile de traiter tels quels un nombre illimité de mots au niveau du mot
  • Les modèles au niveau du caractère doivent apprendre le sens de tous les mots depuis zéro, et ont le problème de réduire la taille effective du contexte d’un facteur correspondant à la longueur moyenne des mots
  • Les modèles comme GPT-2 utilisent le BPE, qui crée des tokens à partir de fragments de mots
    • Les mots fréquents peuvent devenir un token unique
    • Les mots rares sont découpés en fragments plus petits
    • Par exemple, nicholas peut être découpé en nich, o, las
  • L’algorithme BPE classique fusionne de manière répétée des paires de tokens adjacents
  • Pour réduire la taille du code, cette implémentation C utilise une méthode récursive potentiellement exponentielle plutôt qu’un algorithme linéaire
    • Elle cherche une entrée du vocabulary correspondant au préfixe du mot courant
    • Elle tokenise récursivement le reste de la chaîne
    • Elle choisit la meilleure tokenisation selon la longueur et l’index dans le vocabulary

Chargement des poids

  • Les poids du réseau neuronal doivent être lus depuis le disque, et le fichier est un format de sérialisation binaire plat en floats 32 bits
  • Les tailles de modèles GPT-2 utilisent la même architecture et les poids sont stockés dans le même ordre ; il suffit donc de lire les matrices de forme correcte dans l’ordre
  • L’ordre de stockage des couches diffère de ce qu’on attendrait
    • Après les couches 0 et 1 vient la 10
    • C’est dû au tri des noms en lexicographic order
    • Dans un tri de chaînes, 10 vient avant 2
  • L’implémentation utilise du code de permutation pour convertir cet ordre en ordre réel des couches

Chargement du vocabulary BPE

  • Pour exécuter le BPE, il faut d’abord lire le fichier de vocabulary depuis le disque
  • Le fichier d’origine est dans un format destiné à être lu par Python, et non dans un format facile à parser avec un petit code C
  • Le fichier n’est pas une liste de mots, mais une liste de fusions BPE
    • Par exemple, au lieu de stocker directement le token Hello, il indique qu’il faut fusionner H et ello
  • Le fichier utilise un encodage proche de l’UTF-8, mais pas exactement identique
    • Les caractères ASCII imprimables sont stockés tels quels
    • Les caractères non imprimables dans la plage 0 à 31 sont encodés comme 188 + valeur du caractère
    • Par exemple, l’espace est encodé comme le token Ġ
  • Sur disque, Ġ correspond à 0xc4 0xa0 en UTF-8 ; il faut donc un traitement séparé pour le reconvertir en espace

Ce que montre ce petit code

  • Des décennies de progrès en machine learning peuvent être condensées en quelques milliers d’octets de code
  • Si l’on exclut les poids réels du modèle, presque aucun des éléments nécessaires à l’exécution d’un réseau neuronal moderne ne manque
  • Cette implémentation a surtout été créée pour s’amuser, mais elle montre qu’un réseau neuronal peut en pratique s’exécuter à partir de composants simples

1 commentaires

 
GN⁺ 2024-12-13
Avis de Hacker News
  • Je n’ai pas essayé d’exécuter le code moi-même, mais le fait qu’il soit petit est impressionnant.
    Quand on pense que les premiers programmes ELIZA étaient plus gros, cela veut dire qu’en quatre ans on est devenu capable de faire tenir ce genre de chose au niveau de quelques octets.
    Si quelqu’un sait où se cache la magie, j’aimerais bien qu’il l’explique. Je me demande si c’est la fonction GELU, ou bien le modèle téléchargé par le script Bash.

    • L’essentiel de la magie se trouve dans le fichier de modèle de 475 Mo téléchargé par le script Bash.
    • Je l’ai lancé, et ce n’était pas très impressionnant.
      À Who are you?, il répond I am Alice., et si on lui pose des questions sur les ordinateurs ou ses capacités, il répète I am a computer model trained by OpenAI. How can I help you?.
      Si on lui demande d’expliquer une addition, il donne une explication de multiplication, et pour 2+2 ou Sum 2+2, il se contente de répéter tel quel.
  • Je me souviens avoir bricolé avec GPT-2 à sa sortie.
    Avec un ami, on avait exporté nos logs de chat et affiné GPT-2 pour qu’il imite nos conversations, et c’était à la fois hilarant et, parfois, troublant de précision.
    Je me demande à quoi tenait le saut spectaculaire entre GPT-2 et GPT-3. Un modèle plus grand, davantage de données, ou les deux ?
    Je sais que le RLHF a fait une grosse différence, mais même le modèle GPT-3 de base, avec suffisamment d’exemples, était déjà assez utile en simple complétion de texte.

  • Je ne sais pas trop, mais il y a des contes que GPT-2 a écrits et que j’aime beaucoup.
    https://deepdreams.stavros.io/episodes/the-princess-the-fair...

    • C’est vraiment bon, réellement amusant, et c’est aussi une bonne histoire pour s’endormir en l’écoutant.
      Je me demande si elle a été faite avec le GPT-2 de cette page.
    • C’est impressionnant, étrange, mais cohérent à environ 90 %, ce qui crée une ambiance bizarre très particulière.
  • Le passage disant que « c’est surtout fait pour s’amuser, mais c’est un bon exemple de la simplicité réelle des réseaux de neurones » est amusant.
    Chut, ne le dites à personne. L’intelligence artificielle, c’est de la magie noire utilisée pour gagner de l’argent.

  • Est-ce que GPT-2 a été tuné sur des instructions pour être utilisable en vrai chat ?
    Sinon, appeler ça un clone de ChatGPT me paraît assez exagéré.

    • L’article le dit déjà : si l’on ne se soucie pas de la qualité de sortie, on peut fabriquer quelque chose qui ressemble à ChatGPT, et même si la sortie est objectivement assez horrible, ça tourne.
      En pratique, c’est inutilisable et ça n’a presque aucun rapport avec ChatGPT, à part l’emprunt du nom. Mais c’est quand même un programme qui compile et s’exécute.
      Quand on voit des réactions qui encensent les performances d’un projet dont l’auteur reconnaît lui-même qu’il ne fonctionne pas correctement, on a l’impression que l’essentiel est surtout d’attirer l’attention avec un mot à la mode.
  • La phrase « Vous voyez, langages avec de vrais macros ? Lisp n’est pas toujours meilleur que C ! » est acceptable cette fois-ci. C’est une blague qui vise vers le haut.
    Si vous n’avez pas vu le lien vers le code, il est enfoui dans le texte : https://github.com/carlini/c-chat-gpt-2

  • J’ai déjà vu mieux comme chatbot d’intelligence artificielle classique.
    https://www.cs.cmu.edu/afs/cs/project/ai-repository/ai/areas...
    Splotch se compile bien sur les Unix modernes avec seulement quelques petites corrections.

  • Je me demande si quelqu’un l’a lancé en local pour voir quelles sorties ce GPT-2 produit.

    • J’ai l’impression qu’il produit presque toujours les mêmes sorties en boucle.
      Cela reste assez intéressant, et j’aimerais bien mettre le nez dedans et l’ajuster moi-même. Cela fait un moment que j’avais envie de bricoler avec GPT-2 en local.
    • D’après ma lecture, si l’on utilise la même température et la même graine, le modèle GPT-2 chargé de façon classique et celui chargé par ce programme devraient produire exactement la même sortie.
      Je n’ai pas réussi à vérifier directement la température et la graine dans le code ; je regardais surtout pourquoi il avait été obfusqué.
      Même désobfusqué, le code ne devrait pas devenir énormément plus long, et s’il fait environ 10 000 caractères, ce serait déjà suffisamment impressionnant à voir à l’écran.
  • Aujourd’hui, avec gptscript, on peut vite implémenter son propre ChatGPT.
    https://github.com/gptscript-ai/gptscript

  • GELU a vraiment quelque chose de magique :
    UNARY(GELU, b / 2 * (1 + tanh(.7978845 * (b + .044715 * b * b * b))))

    • Ce n’est qu’une approximation pratique de la vraie définition mathématique de GELU.
      La définition est GELU(x) := x * Φ(x), où Φ(x) est la fonction de répartition de la loi gaussienne.
    • Ça rappelle l’inverse rapide de la racine carrée.