1 Punkte von GN⁺ 2024-02-25 | 1 Kommentare | Auf WhatsApp teilen
  • Nur mit PostgreSQL und pgvector wurde der GPT-2-Inferenzablauf in SQL übertragen und von der Tokenisierung über Transformer-Blöcke bis zur Generierung des nächsten Tokens nachgebildet
  • Generative LLMs sind eher eine deterministische Funktion, die für dieselbe Eingabe dieselben Wahrscheinlichkeiten für Kandidaten-Tokens liefert; unterschiedlich werden Antworten erst im probabilistischen Auswahlschritt für das nächste Token
  • Die Implementierung bildet den BPE-Tokenizer von GPT-2, 50257 Tokens, 768-dimensionale Embeddings, 1024 Tokens Kontext, 12 Blöcke, 12 Attention-Heads und ein GELU-basiertes Feedforward-Netz mit SQL-Abfragen und Tabellen ab
  • Wegen Einschränkungen wie fehlender Unicode-Eigenschaften in PostgreSQL-Regulärausdrücken und Grenzen von EXP bei sehr kleinen Werten waren praxisnahe Workarounds für die Datenbankumgebung nötig
  • Im Beispiel wurden aus "Happy New Year! I wish you" 10 Tokens erzeugt, mit der Ausgabe "Happy New Year! I wish you all the best in your new year!"; in der Umgebung des Autors dauerte das 2 Minuten 44 Sekunden

Aufbau einer GPT-2-Inferenzpipeline in SQL

  • ChatGPT antwortete, SQL sei nicht geeignet, um ein großes Sprachmodell zu implementieren, aber hier wurde eine GPT-2-Inferenzpipeline in PostgreSQL-SQL umgesetzt
  • Als Referenz diente Jay Modys GPT in 60 Lines of NumPy; die gleichen Bausteine wurden in Datenbanktabellen und Abfragen übertragen
  • Ein generatives LLM lässt sich wie eine Funktion der Form llm(prompt: str) -> list[tuple[str, float]] betrachten
    • Die Eingabe ist ein Text-Prompt
    • Die Ausgabe ist ein Array aus möglichen nächsten Strings und ihren Wahrscheinlichkeiten
    • Wenn interne Mathematik und Parameter gleich sind, liefert dieselbe Eingabe dasselbe Ergebnis
  • Dass Produkte wie ChatGPT auf dieselbe Frage unterschiedliche Antworten geben können, liegt weniger am Modell selbst als an der probabilistischen Auswahl des nächsten Tokens

Schleife zur Textgenerierung

  • Im Generierungsprozess wird ein Prompt zuerst in ein Token-Array umgewandelt, dann das Modell wiederholt aufgerufen, das nächste Token gewählt und an den Prompt angehängt
  • Der grundlegende Ablauf besteht aus folgenden Schritten
    • tokenize(prompt) wandelt den String in ein Array von Token-IDs um
    • gpt2(tokens) berechnet Wahrscheinlichkeiten für 50257 Tokens
    • select_next_token(candidates) wählt das nächste Token aus
    • Das gewählte Token wird dem Array hinzugefügt
    • Bei Bedingungen wie festgelegter Token-Zahl, Timeout oder Stopword wird abgebrochen
    • detokenize(tokens) stellt aus dem Token-Array wieder einen String her
  • Die so aufaddierte Token-Sequenz kann zu natürlichem Text werden, der Eigenschaften wie Grammatik, Syntax, Bedeutung und scheinbares Schließen zeigt

BPE-Tokenizer in SQL implementieren

  • Vor der Eingabe ins neuronale Netz muss Text in eine Zahlenliste umgewandelt werden; rohe Unicode-Codepoints wären dabei für Token-Raum und Länge ineffizient
  • GPT-2 verwendet eine Variante von Byte pair encoding
    • Das Token-Vokabular verwendet 50257 Codepoints
    • Es enthält UTF-8-Byte-Sequenzen und ein „end of text“-Token
    • Zunächst wird mit 256 Byte-Tokens begonnen, dann werden häufig auftretende benachbarte Token-Paare als neue Tokens hinzugefügt
    • Diese Zusammenführung wird 50000-mal wiederholt, sodass 50256 Tokens entstehen; anschließend wird das End-of-Text-Token hinzugefügt
  • Der GPT-2-Tokenizer hat eine zusätzliche Ebene, die Bytes auf String-Zeichen abbildet; diese Zuordnung ist in OpenAIs GPT-2-encoder.py definiert
  • In der SQL-Implementierung wurde das von OpenAI heruntergeladene Token-Vokabular in die Tabelle tokenizer geladen und die Byte-Zeichen-Zuordnung in der Tabelle encoder gespeichert
  • Im Beispiel "Mississippilessly" wird mit einer rekursiven CTE bei einzelnen Bytes begonnen und dann wiederholt das beste zusammenführbare Nachbarpaar verschmolzen
    • Dabei sinkt die Token-Zahl im Beispiel von 17 auf 5
    • Statt des Unicode-Raums mit rund 150k Codepoints wird der GPT-2-Token-Raum mit rund 50k Tokens genutzt
  • Bei mehreren Wörtern zerlegt GPT-2 den Text per Regulärem Ausdruck und führt die Merges innerhalb jedes Wortes aus
    • PostgreSQL unterstützt in Regulären Ausdrücken keine Unicode-Zeicheneigenschaften, daher wurde der ursprüngliche GPT-2-RegEx teilweise angepasst
    • Diese Änderung könnte die korrekte Unicode-Unterstützung beeinträchtigt haben
  • "PostgreSQL is great" wird im SQL-Tokenizer zu [6307, 47701, 318, 1049]
    • Die Token-Cluster sind Post, greSQL, Ġis, Ġgreat
    • Ġ steht für ein Leerzeichen

Embeddings und Kontextfenster

  • Token-IDs werden nicht direkt in den Modellberechnungen verwendet, sondern in Embedding-Vektoren umgewandelt
  • GPT-2 verwendet getrennte Embeddings für Tokens und Positionen
    • WTE ist das Word-Token-Embedding und eine 50257×768-Matrix
    • WPE ist das Word-Position-Embedding und eine 1024×768-Matrix
  • Für jede Token-Position werden der WTE-Vektor und der WPE-Vektor addiert und bilden so den Eingabevektor für den nächsten Schritt
  • Weil WPE nur 1024 Positionen hat, können in einem GPT-2-Prompt maximal 1024 Tokens verwendet werden
    • Diese Zahl entspricht dem Kontextfenster des LLM
    • Sie ist ein beim Modelldesign festgelegter Hyperparameter und ändert sich nicht durch Training
  • Die SQL-Implementierung verwendet pgvector
    • Vektoroperationen könnten auch direkt auf Arrays in reinem SQL definiert werden, wären dann aber langsamer
    • Eine frühe Version funktionierte mit reinen SQL-Funktionen, war jedoch langsam

Self-Attention als SQL-Abfragen entfalten

  • Der Kern des Transformers ist der Self-Attention-Mechanismus auf Basis der Arbeit Attention is all you need von 2017
  • Attention sorgt dafür, dass Token-Vektoren einander beeinflussen, sodass Informationen vom Anfang des Prompts bis zum letzten Vektor weitergegeben werden
  • Die GPT-2-Implementierung verwendet 12 Sätze von Q-, K- und V-Matrizen
    • Jeder Satz ist ein Attention-Head
    • Jeder Head ist 64-dimensional
    • c_attn ist eine lineare Transformation 768×2304; das Ergebnis ist ein 2304-dimensionaler Vektor, in dem Q, K und V horizontal gestapelt sind
    • Gewichte und Biases liegen in den Tabellen c_attn_w und c_attn_b
  • Vor der Attention-Berechnung wird Layer Normalization angewendet
    • Scale- und Shift-Parameter liegen in den Tabellen ln_1_g und ln_1_b
  • Bei der kausalen Self-Attention wird eine kausale Maske verwendet, damit spätere Tokens frühere Tokens nicht beeinflussen können
    • Die Kandidaten für das nächste Token werden letztlich aus dem letzten Embedding bestimmt
    • Der Informationsfluss muss in Richtung des letzten Vektors laufen, und Zwischenwerte des letzten Vektors dürfen frühere Vektoren nicht beeinflussen
  • In der SQL-Implementierung werden bei der Softmax-Berechnung Eingaben kleiner als -745.13 als 0 behandelt, um zu vermeiden, dass PostgreSQL-EXP bei sehr kleinen Zahlen scheitert
  • Dank der kausalen Maske ändern sich die Berechnungsergebnisse für frühere Tokens nicht, wenn ein neues Token an den Prompt angehängt wird
    • Die originale GPT-2-Implementierung nutzt diese Eigenschaft aus
    • Die SQL-Implementierung verzichtet der Einfachheit halber auf diese Wiederverwendung

Multi-Head-Attention und Residualverbindungen

  • Die Attention-Ergebnisse der 12 Heads sind jeweils 64-dimensional; horizontal gestapelt ergeben sie wieder 768 Dimensionen
  • Anschließend wird die Attention-Ausgabe mit der gelernten linearen Transformation aus c_proj_w und c_proj_b projiziert
  • Zum Ergebnis der Multi-Head-Attention wird die ursprüngliche Eingabe wieder addiert
    • Diese Residualverbindung ist ein Verfahren aus dem ursprünglichen Transformer-Paper
    • Sie wurde eingeführt, um beim Training Probleme wie Vanishing und Exploding Gradients abzumildern

Feedforward-Schritt und Transformer-Block

  • Auf die Attention folgt ein Feedforward-Neuralnetz
  • Der Feedforward-Schritt von GPT-2 besteht aus einem dreischichtigen Multi-Layer-Perceptron
    • Die Dimensionen sind 768 → 3072 → 768
    • Als Aktivierungsfunktion wird GELU verwendet
  • Die Parameter der linearen Transformationen sind in folgenden Tabellen gespeichert
    • mlp_c_fc_w, mlp_c_fc_b
    • mlp_c_proj_w, mlp_c_proj_b
  • Auch die Feedforward-Eingabe wird zunächst mit den ln_2-Parametern normalisiert
    • ln_2_g und ln_2_b speichern Scale und Shift
  • Auch auf die Feedforward-Ausgabe wird eine Residualverbindung angewendet, bei der die Eingabe erneut addiert wird
  • Diese Kombination aus Attention und Feedforward bildet einen Block, und GPT-2 verbindet 12 solcher Blöcke wie eine Pipeline
    • Jeder Block besitzt seinen eigenen Satz gelernter Parameter
    • In SQL werden die Blöcke mit einer rekursiven CTE verbunden
  • Die Ausgabe des letzten Blocks wird erneut mit den ln_f-Parametern normalisiert

Rückübersetzung zum nächsten Token

  • In der finalen Ausgabe ist der 768-dimensionale Vektor an der letzten Position das Embedding des nächsten Tokens
  • Um diesen Vektor wieder in ein Token zurückzuführen, wird erneut die WTE-Matrix verwendet, die auch für das anfängliche Token-Embedding genutzt wurde
  • Eine exakte Rücktransformation ist meist nicht möglich
    • Das vorhergesagte Embedding muss nicht exakt einer bestimmten Zeile in WTE entsprechen
    • Deshalb wird das Dot Product mit jedem Token-Embedding berechnet, um nahe Tokens zu finden
  • Die Dot Products von WTE und dem vorhergesagten Embedding ergeben 50257 Scores, also Logits
  • Diese Scores werden per Softmax in Wahrscheinlichkeiten umgewandelt
    • Die Zahl der Top-Kandidaten ist top_n
    • Der Wert zur Steuerung der Wahrscheinlichkeitsverteilung ist die Temperature
    • Je höher die Temperature, desto wahrscheinlicher wird die Auswahl eines nicht erstplatzierten Tokens und desto weniger vorhersagbar wird die Inferenz
  • Im Beispiel "PostgreSQL is great" sind die Top-5-Kandidaten für das nächste Token:
    • Ġfor
    • ,
    • .
    • Ġat
    • Ġto
  • Wenn die Temperature auf 0.5, 1 oder 2 geändert wird, verändert sich die Softmax-Verteilung derselben Kandidaten entsprechend

Reale Inferenz-Ergebnisse und Code

  • Das finale SQL wiederholt den Ablauf, Tokens entsprechend ihren Wahrscheinlichkeiten auszuwählen und an den Prompt anzuhängen
  • Das Modell selbst ist deterministisch; der nichtdeterministische Anteil steckt nur im Zufall bei der Token-Auswahl
  • Die Beispielkonfiguration ist wie folgt
    • Prompt: "Happy New Year! I wish you"
    • Anzahl generierter Tokens: 10
    • Temperature: 2
    • top_n: 1
    • Verwendung von SETSEED(0.20231231)
  • In der Umgebung des Autors lief die Abfrage 2 Minuten 44 Sekunden
  • Die Ausgabe lautete "Happy New Year! I wish you all the best in your new year!"
  • Die Query und der Installationscode stehen im GitHub-Repository quassnoi/explain-extended-2024

1 Kommentare

 
GN⁺ 2024-02-25
Meinungen auf Hacker News
  • Wunderschön. Ich habe mich selbst in einem ähnlichen Kaninchenbau mit SQLite vergraben, bin aber noch nicht so weit gekommen, neuronale Netze hineinzuziehen.
    Inspiriert hat mich die makemore-Vorlesungsreihe[0]; nach etwa einer Stunde geht sie vom Counting-Ansatz zu neuronalen Netzen über, und ungefähr bis dorthin bin ich gekommen.
    Das in ein relationales Modell zu zerlegen, ist eine wirklich gute Übung.
    [0] https://www.youtube.com/watch?v=PaCmpygFfXo

    • Wenn man weiter schaut, leitet das neuronale Netz genau dieselbe Tabelle wie der Counting-Ansatz ab und liefert beim Generieren auch exakt dieselben Ergebnisse.
  • Gute Demo, aber die Erklärung zu kausalem Masking scheint im Artikel Training und Inferenz zu vermischen.
    Kausales Masking dient beim Training dazu, zu verhindern, dass das Modell zukünftige Tokens „erspäht“, und in GPT-artigen Architekturen auch dazu, bei der Inferenz die Autoregressivität zu erzwingen.
    Bei der Inferenz wird ohnehin nur das letzte Token verwendet; dieses Token richtet seine Aufmerksamkeit auf die gesamte Eingabesequenz. Daher wird das nächste Token nicht nur aus dem Embedding des letzten Tokens bestimmt.

  • Ich frage mich, ob das die Driver-Loop von GPT korrekt abbildet: Es sieht nach einer Struktur aus, bei der der Prompt tokenisiert wird, man mit gpt2(tokens) Wahrscheinlichkeiten für 50.257 Tokens erhält, das nächste Token auswählt, es an die Tokenliste anhängt, die Abbruchbedingung prüft und am Ende wieder detokenisiert.
    Aber das wirkt, als würde eine Zustandsmaschine den Shlemiel-the-painter-Algorithmus implementieren, und wirft Fragen zu den inhärenten Rechenkosten der Generierung auf.

    • Unter dem Kontextfenster, von dem Leute bei großen Sprachmodellen sprechen, verstehe ich, dass es eine Obergrenze für die Anzahl der vorgehaltenen Tokens gibt und die ältesten Tokens verworfen werden.
      Dieses Fenster ist ein Sliding Window.
    • Genau, das ist die Schleife, und die ganze Magie steckt in der Funktion gpt2.
    • Das ist nur ein winziger Teil des Algorithmus.
      Es zeigt eher nur, wie die generierten Tokens zu einem Satz zusammengesetzt werden.
  • Verwandtes Material: A GPT in 60 Lines of NumPy - https://news.ycombinator.com/item?id=34726115 - Februar 2023, 146 Kommentare

    • Dieses Material wird bereits am Anfang des Artikels erwähnt.
  • In einem ähnlichen Kontext habe ich GPT vollständig mit Tabellenkalkulationsfunktionen implementiert und dazu auch ein begleitendes Video-Tutorial erstellt.
    https://spreadsheets-are-all-you-need.ai/

    • Das erste Video ist hervorragend.
      Aus der Perspektive von jemandem, der LLMs ziemlich cool findet, aber beruflich nicht lernen musste, wie sie tatsächlich funktionieren, hat mir dieses 10-Minuten-Video mehr beigebracht als Jahre des Lesens schwer verständlicher HN-Kommentare und oberflächlicher Mainstream-Artikel.
      Wenn man sieht, wie eine riesige Menge an Gleitkommazahlen darauf wartet, berechnet zu werden, versteht man auch viel intuitiver, warum diese Technik so viele GPUs verschlingt.
    • Tabellenkalkulationen sind eine natürliche Art, LLMs zu erklären.
      Wenn man für jedes Trainingsbeispiel die Ableitung jedes Parameters berechnet und explizit zeigt, wie sie auf den jeweiligen Parameter abgebildet wird, ließe sich wohl auch der Trainingsprozess gut erklären.
  • Schön. Was noch vor einem Jahr wie eine Art Magie wirkte, wird jetzt so gut erklärt, dass es fast sogar ein Kind nachvollziehen könnte.

    • Diese Magie hat nicht erst vor einem Jahr begonnen.
      Das im Artikel erklärte Modell ist GPT-2, das Anfang 2019 veröffentlicht wurde.
    • „Sogar ein Kind nachvollziehen könnte“ stimmt so nicht.
      Um diesen Artikel richtig zu verstehen, braucht man solide Informatik-Grundkenntnisse, und schon der Titel selbst ist für 99 % der Menschheit wenig zugänglich.
  • Ich habe GPT und LLMs komplett gemieden, aber dieser Ansatz scheint zwar eine gewisse Sprachflüssigkeit bei Textausgaben erzeugen zu können, nicht jedoch die Fähigkeit, Fragen zu interpretieren und zu beantworten.
    Ich frage mich, ob es einfache Blogbeiträge oder Kurse gibt, die erklären, wie das tatsächlich funktioniert, oder eine Spielzeug-Engine etwa in Python zeigen.
    Die Lernmaterialien, die ich bisher gesehen habe, konzentrieren sich darauf, wie man Plattformen nutzt, und behandeln die interne Funktionsweise kaum.

  • Interessanterweise erfordert modernes Machine Learning keine Turing-Vollständigkeit.
    Und dennoch wird die Möglichkeit von AGI in Betracht gezogen; es wäre also ziemlich interessant, wenn sich herausstellen sollte, dass Turing-Vollständigkeit nicht nötig ist.

    • Turing-Vollständigkeit scheint nötig zu sein.
      Aus einem einfachen Grund: Ich kann im Kopf die Ausführung von Turing-vollständigem Code nachvollziehen.
    • Token-Inferenz selbst ist nicht Turing-vollständig, aber wenn die Ausgabe Nebenwirkungen erzeugen kann, etwa den Prompt der nächsten Iteration zu ändern, ist das eine völlig andere Geschichte.
  • Der Artikel war hervorragend; die Erklärungen der einzelnen Komponenten waren klar und ziemlich gründlich, daher gut zu lesen.
    Allerdings habe ich versehentlich auf „+ expand source“ geklickt, und nachdem ich dieses erstaunliche Monster gesehen hatte, konnte ich ChatGPT zustimmen, wenn es sagt: „SQL ist für die Implementierung großer Sprachmodelle nicht geeignet.“

    • Ich habe auch darauf geklickt und keine Möglichkeit gefunden, es wieder einzuklappen.
  • Die Aussage „normales Unicode passt nicht gut zu neuronalen Netzen“ stimmt nicht. Siehe zum Beispiel ByT5.
    Was der Artikel „Alphabet“ nennt, heißt normalerweise Vokabular; wenn man UTF-8-Bytes als Vokabular verwendet, hat man nicht 149.186 Tokens, sondern 256.
    Genau das macht ByT5.

    • Der Punkt ist nicht, dass es gar nicht geht, sondern dass es nicht so gut funktioniert wie andere Ansätze, die wir haben.
      Dass alle leistungsstärksten Modelle am Markt Tokenisierung verwenden, ist der Beleg dafür.
      Es ist kein Geheimnis, dass Tokenisierung im Grunde eher ein Hack ist und man sie idealerweise irgendwann auf die eine oder andere Weise loswerden möchte (https://twitter.com/karpathy/status/1657949234535211009).
      Grundsätzlich kann man die Schwächen der Byte-Level-Tokenisierung durch größere Modelle und größere Kontexte ausgleichen, aber in der Praxis braucht es viel mehr Ressourcen, um ein Modell mit demselben Intelligenzniveau zu trainieren.
      Natürlich gibt es auch bestimmte Aufgaben, bei denen Tokenisierung der Intelligenz eher schadet, etwa beim Zählen der Buchstaben eines Wortes.