Show HN: Möglichkeit, die LLM-Inferenzgeschwindigkeit zu verdoppeln (Speeding up LLM inference 2x times (possibly))
(asciinema.org)Was ist Effort?
- Effort bezeichnet die Energie, Zeit und Ressourcen, die eingesetzt werden, um etwas zu erreichen oder ein Ziel zu verwirklichen
- Gemeint sind die Anstrengung und Mühe, die nötig sind, um Hindernisse zu überwinden oder ein gewünschtes Ergebnis zu erzielen
- Effort kann physischer oder mentaler Natur sein
- physische Anstrengung wie die Energie, die zum Heben schwerer Gegenstände oder zum Laufen eines Marathons benötigt wird
- mentale Anstrengung wie Konzentration und geistige Kraft, die zum Lösen komplexer Probleme oder zum Erlernen neuer Technologien erforderlich sind
- In vielen Lebensbereichen wie persönlicher Entwicklung, Beruf und Bildung ist Effort ein zentraler Faktor für den Erfolg
Meinung von GN⁺
- Effort ist nicht nur die Menge an eingesetzter Zeit oder Energie, sondern ein Konzept, bei dem verschiedene Faktoren wie Ausdauer, Konzentration und Motivation zusammenwirken. Deshalb ist Erfolg nicht automatisch garantiert, nur weil man sich sehr anstrengt
- Besonders bei Tätigkeiten, die mentale Anstrengung erfordern, sollte man darauf achten, nicht in ein Burnout zu geraten. Wichtig ist, Wege zu finden, Effort durch angemessene Erholung und Belohnung aufrechtzuerhalten
- Die Art, wie Menschen Effort einsetzen, kann unterschiedlich sein. Manche arbeiten kurzfristig sehr intensiv, andere investieren über lange Zeit hinweg kontinuierlich Effort. Es ist effektiv, eine Vorgehensweise zu finden, die zu einem selbst passt
1 Kommentare
Hacker-News-Kommentare
Der Kern des Algorithmus scheint darin zu bestehen, Parameter während der Ausführung zu prunen, innerhalb jeder Gruppe anhand der Rangfolge der Absolutwerte der zu prunenden Gewichte die weniger wichtigen Gewichte zu bestimmen, sie auf 0 zu setzen und so die Gewichtsmatrix sparse zu machen.
Wenn man nach Model Pruning sucht, findet man viele Ergebnisse; auch https://arxiv.org/abs/2305.11627 behandelt „magnitude-based pruning“ als Baseline und zitiert https://arxiv.org/pdf/2301.00774.pdf.
Ich mag es nicht, wenn Paper ihre Baselines schlampig implementieren und dann ihre eigene Methode anpreisen oder sie mit jeder Menge mathematischer Terminologie verpacken. Der Blogpost im Originaltext vermittelt die Methode viel leichter verständlich, selbst für Leute mit kaum Vorwissen.
Die Ansätze in den erwähnten Papern wirken statisch, und sie scheinen keinen Algorithmus zu liefern, der die tatsächliche Berechnung mit 20–50%-Ergebnissen beschleunigt. Das war ein großer Teil des schwierigen Problems. Irgendwann will ich mir die einschlägige Literatur gründlich ansehen.
Letztlich möchte ich zusammen mit den Papern, die Leute in den Kommentaren gepostet haben, eine Zitationsseite hinzufügen. Ich denke, dass vielleicht bald jemand einen Text findet, der diesen Algorithmus bereits beschrieben hat.
Während der Entwicklung habe ich auch gpt-4 gefragt und gegoogelt, aber das, was ich gefunden habe, war meist statisch oder lief darauf hinaus, ganze Dimensionen/Layer willkürlich zu entfernen und anschließend neu zu trainieren. Etwas, das exakt zu dieser Idee passt, habe ich nicht gefunden.
Je dichter und schwerer verständlich ein Text ist, desto größer ist die Wahrscheinlichkeit, dass darin schlechte Wissenschaft versteckt wird.
Mir gefiel dieser Satz im Abschnitt zur GPU-Implementierung:
„Leser, die neu in der GPU-Programmierung sind, fragen sich jetzt vielleicht: Wie funktioniert das?
Leser mit Erfahrung in der GPU-Programmierung fragen sich vielleicht: Wie zum Teufel funktioniert das?“
Es wäre gut, wenn jemand, der Metal wirklich gut kennt, diesen Code reviewen würde. Das war mein erster Versuch mit GPU-Programmierung.
„Drehen wir die Matrix um, sortieren die Elemente zeilenweise und betrachten die Multiplikation erneut aus dieser Richtung. Kluge Leute nennen das Compressed Sparse Row (CSR)-Format. Um nun zu multiplizieren, nimmt man die 1 aus dem Vektor, multipliziert sie mit 256 und addiert sie zur dritten Zeile des Ausgabevektors, und so weiter. Sehen wir uns jetzt an, was passiert, wenn wir die letzte Spalte mit den niedrigsten Werten abschneiden.“
Ich frage mich, wie das mit der Art zusammenhängt, wie CSR die Anzahl der Multiplikationen reduziert.
Dieser Ansatz wirkt ähnlich wie semistrukturierte Sparsity, die sogenannte 2:4-Sparsity, und ein expliziter Vergleich wäre sinnvoll. Nach meinem Überfliegen ist diese Technik für Apple Silicon optimiert, erreicht bei 75% Sparsity etwa 2× Geschwindigkeit, wird abhängig vom Input dynamisch zur Laufzeit angewandt und erlaubt es, den Grad der Sparsity zu wählen.
2:4-semistrukturierte Sparsity ist dagegen für GPUs mit Sparse Tensor Cores optimiert, also Nvidia Ampere und neuer, erreicht bei 50% Sparsity etwa 2× Geschwindigkeit, wird statisch auf den gespeicherten Modellzustand angewandt und liefert bei 50% Sparsity wahrscheinlich schlechtere Ergebnisse als diese Technik.
Der Vergleich, den ich sehen möchte, ist zwischen den Ergebnissen von semistrukturierter Sparsity mit 50% Sparsity und 2× Speedup und denen dieser Technik mit 75% Sparsity und 2× Speedup.
Ich habe Apple Silicon gewählt, weil es leicht zu entwickeln war. Es ist gut möglich, dass dieser Algorithmus auch auf anderen Architekturen gut performt.
Aus der Perspektive von jemandem, der CSR verwendet hat, ist das nicht überraschend. Unter neueren Formaten gibt es möglicherweise solche wie block ELL, die besser zu Hardware-Eigenschaften passen.
Solche Formate vermeiden nicht zusammengeführte Lesezugriffe oder Gather, aber der Code wird schwieriger.
bucketMul hat kaum nicht zusammengeführte Lesezugriffe und verwendet eine andere Datenstruktur als gewöhnliches CSR. Das ist hier beschrieben: https://kolinko.github.io/effort/bucketmul.html
Jede Matrixzeile wird in 16 Teile aufgeteilt, und es wird ausgewählt, welche Teile gelesen werden sollen. Schreibzugriffe sind vollständig linear.
Allerdings bin ich mir gerade nicht sicher, ob das, was ich sage, Sinn ergibt. Es ist heute schon spät, und es war ein langer Tag.
Gute Idee und guter Text. Ich arbeite ebenfalls im Bereich Sparsity bei der Inferenz neuronaler Netze, und mir fallen ein paar Punkte ein, die man im Hinterkopf behalten sollte.
Im Vergleich zu einer dichten Implementierung der Matrix-Vektor-Multiplikation erhöht dieser Algorithmus die algorithmische Komplexität, reduziert aber den Speicherverkehr. Matrix-Vektor-Multiplikation ist normalerweise speichergebunden; weniger Speicherzugriffe erhöhen daher den Durchsatz. Sobald die Batch-Größe jedoch größer als 1 wird, ist der Speicherzugriff nicht mehr der Flaschenhals, und der Geschwindigkeitsgewinn dürfte sehr schnell verschwinden.
Als Vergleich würde ich nicht nur dasselbe Modell sehen wollen, sondern auch andere Modelle mit einer 2× schnelleren Architektur. Zum Beispiel wäre interessant, wie sich diese Methode mit 50 % Sparsity auf ein LLM mit 13B Parametern im Vergleich zu einem LLM mit 7B Parametern schlägt, oder im Vergleich zu demselben LLM, das auf die halbe Bitbreite der Referenz quantisiert wurde. Wenn sie in derselben Zeit Ausgaben mit höherer Wiedergabetreue als bestehende Inferenz-Frameworks liefern kann, wäre das Stoff für ein spannendes Paper.
Da Multiplikationen ausgelassen werden, dürfte der Approximationsfehler systematisch dazu tendieren, dass der Betrag kleiner ist als beim tatsächlichen Ergebnis. Wenn man einen Korrekturterm hinzufügen könnte, der diesen systematischen Fehler ausgleicht, würde die Leistung wohl noch etwas besser werden.
Big-O-Notation passt nicht besonders gut zu GPU-Arbeit, aber in diesem Fall stimmt es grob.
Das Hauptproblem sind die architektonischen Grenzen der GPU. Dieser Algorithmus benötigt mehr Register/Threadgroups/Cache-Speicher als der traditionelle Ansatz, und das wird zum wichtigsten Flaschenhals. Außerdem verwendet jede Multiplikation andere Buckets, daher ist es nicht so einfach, die Arbeit wie bei MoE-Modellen zu parallelisieren.
Bei größeren Architekturen habe ich tatsächlich viel mit Mixtral getestet, im Grunde einem 13B-Modell, und nach meinem Eindruck hält es sich dort deutlich besser. Die Inferenzgeschwindigkeit im Verhältnis zum effort bleibt erhalten, und die Qualität im Verhältnis zum effort liefert nicht erst bei 20–25 %, sondern schon bei 12–16 % noch lesbare Ergebnisse. Die Tests waren begrenzt, und beim Einbauen der Mistral-Implementierung habe ich die Mixtral-Implementierung kaputtgemacht, daher habe ich keine belastbaren Daten, aber ich werde das bald beheben.
Intuitiv denke ich, dass man bei größeren Modellen den effort stärker reduzieren kann.
Dass das Auslassen von Multiplikationen einen Bias erzeugen würde, hatte ich anfangs auch vermutet, aber entgegen der Intuition war das nicht der Fall. Ich habe ein paar Diagramme, aber sie sind noch nicht für die Veröffentlichung aufbereitet.
Da die Werte in der Matrix gleichmäßig auf positive und negative Werte verteilt sind, driftet das Ergebnis nach einem bestimmten Schwellwert nicht stark ab.
Sieht großartig aus. Allerdings liegt eine Latenz von 15 ms nahe an den 16,7 ms von vertikaler Synchronisation bei 60 Hz.
Wenn du den Bildschirm bei jedem Token aktualisierst, könnte irgendwo eine Synchronisierung die Ursache sein.
Trotzdem danke für die Idee. Ich nehme sie als ersten Community-Beitrag an :D
Wirklich großartige und offene Arbeit. Ich werde genau beobachten, ob llama.cpp das implementiert.
Ich habe nach Möglichkeiten gesucht, CPU-Inferenz zu beschleunigen, und die Idee von effort gefällt mir wirklich gut.
Den Namen hat chatgpt vorgeschlagen. Da es diesen Ansatz nicht wiedererkennt, könnte er tatsächlich neu sein.
Ich möchte llama.cpp und andere Projekte kontaktieren, und es wäre schön, wenn es implementiert würde. Ich habe auch überlegt, selbst einen Patch für llama zu schreiben, aber C++ und die Größe des Projekts waren mir zu viel.
CPU-Inferenz wird genauso schneller werden. Außerdem kann man nur einen Teil der Gewichte laden, zum Beispiel weniger wichtige überspringen und nur 70 % laden, sodass man ein Modell mit weniger VRAM als zuvor ausführen kann. Eine Q8-Implementierung ist allerdings noch nötig.
Interessanterweise konnte ich, als ich Benchmarks mit llama.cpp vergleichen wollte, keine 7B/FP16-Geschwindigkeit auf einem MB Air mit 16 GB finden. Der Grund ist, dass es auf herkömmliche Weise nicht ausführbar ist. Mit Effort ist es möglich.
Ähnlich habe ich auf einem M2 mit 96 GB ein Mixtral in voller Auflösung, aber zugeschnitten, laufen lassen. Normalerweise wären 114 GB RAM nötig, aber da nur 75 % der Gewichte geladen wurden, lief es flüssig. Im Moment habe ich die Implementierung etwas kaputtgemacht, sodass sie Müll ausgibt; das muss ich beheben.
Guter Artikel. Ich bin wirklich gespannt, wie die Leistung pro VRAM im Vergleich zu einfacher Quantisierung aussieht.
Außerdem frage ich mich, ob eine plattformübergreifende Version geplant ist.
Natürlich kann man auch weniger Gewichte laden, aber wenn man mehr als 20–30 % der Gewichte entfernt, scheint die Qualität schnell schlechter zu werden.
Anders gesagt: Dieser Algorithmus entkoppelt die Inferenzzeit vom VRAM-Verbrauch.
Trotzdem interessiert mich auch, ob ein mit effort auf 75 % zugeschnittenes Q8 bessere Ergebnisse liefern kann als Q6.
Allerdings wird es wohl noch ein paar Wochen dauern, bis die Implementierung ausreichend ausgereift ist, um das sauber zu testen.
Diese Methode kommt ohne erneutes Training aus, aber ich frage mich, ob man einen Teil der verlorenen Qualität zurückholen könnte, wenn man einen solchen Ansatz zusammen mit Quantisierung verwendet und später zusätzlich trainiert.
Es ist schön, so etwas zu sehen, und es ist gut, sich vorstellen zu können, wie stark Leistung und Kosten künftig noch verbessert werden können. Danke, dass du es als Open Source entwickelst.
Die eine ist nachträgliches Post-Training, die andere ist quantisierungsbewusstes Training, bei dem während des Trainings quantisiert wird, Aktivierungen und Gradienten aber in voller Präzision bleiben.