1 Punkte von GN⁺ 2024-10-12 | 1 Kommentare | Auf WhatsApp teilen
  • lm.rs ist ein Projekt, das lokale Sprachmodell-Inferenz auf der CPU in Rust ausführt und eine minimale Code-Implementierung anstrebt, die die gesamte Inferenz ohne ML-Bibliotheken erledigt
  • Inspiriert von Karpathys llama2.c und llm.c; anfangs wurde nur Google Gemma 2 unterstützt, inzwischen wurde die Unterstützung auf Llama 3.2 und Bildeingaben mit PHI-3.5 erweitert
  • Mit der neuesten Änderung wurde Batch-Verarbeitung implementiert, wodurch die Bildkodierung bis zu etwa 3-mal schneller wurde; Llama 3.2 1B läuft auf der 16-Core-Maschine des Autors mit 50 tok/s
  • Vorbereitete Modelle sind auf Hugging Face verfügbar; das README empfiehlt Q8_0 und weist darauf hin, dass die Q4_0-Quantisierung noch verbessert wird
  • Nutzer können Modelle und Tokenizer im LMRS-Format herunterladen und direkt bauen oder die Originalmodelldateien von Hugging Face mit export.py und tokenizer.py konvertieren und ausführen

Was lm.rs erreichen will

  • lm.rs ist eine in Rust geschriebene Implementierung für lokale, CPU-basierte Sprachmodell-Inferenz
  • Ziel ist eine minimale Code-Implementierung, die die vollständige Inferenz eines Sprachmodells auf der CPU ohne ML-Bibliotheken ausführt
  • Inspiriert von Karpathys llama2.c und llm.c
  • Das README sagt, dass der aktuelle Code „nicht ganz so minimal“ sei und dass es bei Teilen des Codes noch Raum für Optimierung und Verbesserung gebe
  • Das Projekt war für den Autor auch ein Anlass, Rust erstmals auszuprobieren

Unterstützte Modelle und multimodale Erweiterung

  • Anfangs wurde nur das Google-Modell Gemma 2 unterstützt, später kam Unterstützung für Llama 3.2 hinzu
  • Kürzlich wurde über PHI-3.5 die Option ergänzt, Bilder zu verwenden
  • Aktuell hervorgehobene Unterstützungen
    • Multimodale Unterstützung über das Modell PHI-3.5-vision
    • Unterstützung für das reine Textmodell PHI-3.5-mini
  • Zugehörige Ressourcen

Performance und vorbereitete Modelle

Ablauf der Modellkonvertierung

  • Wenn man vorbereitete quantisierte Modelle und Tokenizer von Hugging Face bezieht, kann der Konvertierungsschritt übersprungen werden
  • Wer die von Google oder Meta auf Hugging Face veröffentlichten Modelle selbst konvertieren will, muss zusätzliche Python-Abhängigkeiten installieren
pip install -r requirements.txt
  • Verwendet werden die von der Originalmodellseite heruntergeladenen Dateien .safetensors und config.json
  • Für multimodale Modelle wie PHI3.5 Vision wird außerdem die CLIP-config-Datei benötigt
  • export.py konvertiert bfloat16-Gewichte in das LMRS-Format
python export.py --files [ordered .safetensor files] --config [model config.json] --save-path [name and path to save] --type [model type (GEMMA/LLAMA/PHI)]
  • Um eine quantisierte Version zu exportieren, nutzt man die Flags --quantize und --quantize-type
  • Die Größe eines int8-quantisierten Modells kann je nach Gruppengröße von etwa 9,8G auf etwa 2,5G sinken
  • Bei multimodalen Modellen muss das Argument --vision-config enthalten sein
  • tokenizer.py konvertiert ein Tokenizer-Modell in das LMRS-Tokenizer-Format
python tokenizer.py --model-id [huggingface model_id] --tokenizer-type [type of the tokenizer (GEMMA/LLAMA/PHI)]

Build und Ausführung

  • Der Rust-Code wird mit cargo kompiliert; das README nennt ausdrücklich die Übergabe des target-cpu-Flags
RUSTFLAGS="-C target-cpu=native" cargo build --release --bin chat
  • Um multimodale Funktionen zu aktivieren, fügt man das Argument --features multimodal hinzu
  • Die Standardausführung erfolgt durch Angabe der Modellgewichtungsdatei
./target/release/chat --model [model weights file]
  • Als zusätzliche Argumente können tokenizer, temperature, top-p, show-metrics und weitere verwendet werden
  • Verfügbare Argumente lassen sich mit --help prüfen
  • Bei multimodalen Modellen wird der Bildpfad mit dem Argument --image angegeben
  • Bei Verwendung von PHI3.5-vision empfiehlt das README temperature 0

WebUI-Backend ausführen

  • Um das Backend für die WebUI auszuführen, kompiliert man mit der backend-Funktion
RUSTFLAGS="-C target-cpu=native" cargo build --release --features backend --bin backend
  • Für das multimodale Backend wird die Funktion backend-multimodal aktiviert
  • Das Backend wird durch Angabe der Modellgewichtungsdatei ausgeführt
./target/release/backend --model [model weights file]
  • IP und Port können mit --ip und --port geändert werden
  • Zusätzliche Flags wie temperature können ebenfalls verwendet werden
  • Für multimodale Kompatibilität nutzt man das Flag --multimodal
  • Nach dem Start kann man sich mit dem Webinterface verbinden

TODO-Status und Lizenz

  • Abgeschlossene Punkte
    • Weitere Sampling-Methoden hinzugefügt
    • Von den Testpunkten für 9B- und 27B-Modelle wurde der 9B-Test abgeschlossen; 27B ist als vermutlich zu langsam markiert
    • Parallelisierung der Multi-Head-Attention-Schleife
    • Performance-Metriken hinzugefügt
    • Unterstützung für int8- und int4-Quantisierung
  • Verbleibender Punkt
    • Funktion zur Bereitstellung eines System-Prompts
  • Die Lizenz ist MIT

1 Kommentare

 
GN⁺ 2024-10-12
Hacker-News-Kommentare
  • Auf einem M2 64GB MacBook lief die 1,2-GB-Datei llama3.2-1b-it-q80.lmrs gefühlt ziemlich schnell; laut Activity Monitor wurden dabei 13 Threads und 1000 % CPU genutzt
    lm.rs wurde nach /tmp geklont und mit RUSTFLAGS="-C target-cpu=native" cargo build --release --bin chat gebaut; danach wurden tokenizer.bin und llama3.2-1b-it-q80.lmrs von Hugging Face geladen und mit ./target/release/chat --model llama3.2-1b-it-q80.lmrs ausgeführt

    • Es wurde gefragt, ob man mit ./target/release/chat --model llama3.2-1b-it-q80.lmrs --show-metrics prüfen könne, wie viele Tokens pro Sekunde herauskommen
    • Mit einem einfachen französischen Dialog-Prompt schien es zunächst zu antworten, brach dann aber bald in eine endlose, schwer verständliche Müllausgabe zusammen
      Aus Formatgründen blieb nur ein Teil erhalten, aber es war im Wesentlichen eine lange Folge zufälliger Wörter
    • Es stellt sich die Frage, wie man einordnen sollte, wie intelligent das im Vergleich zum aktuellen ChatGPT ist
  • Der Artikel ist sehr gut geschrieben, und Teile des Source Codes könnten sich im Unterricht verwenden lassen, um zu erklären, wie Transformer tatsächlich funktionieren
    Im Vergleich zu Diagrammen von Attention Heads ist der Code konkreter und detaillierter. Allerdings könnte eine Library, die direkt nach stdout schreibt, die Ausgabe von Anwendungen stören, etwa wenn ein Texteditor Stilprüfungen anzeigt; besser wäre es wohl, in den String-Buffer einer Logging-Instanz zu schreiben, die mit dem lm.rs-Objekt verbunden ist
    Außerdem scheint der Model Reader an einer Stelle unsafe zu verwenden, um Daten-Alignment zu erzwingen; es würde interessieren, ob das auch ohne unsafe vernünftig machbar ist

    • Noch besser als ein String-Buffer wäre es, einen User-Callback aufzurufen
      Dann könnte man so etwas wie Log-Ausgaben auch in einer GUI anzeigen
  • Es wurden bereits einige Werkzeuge in Rust für Model Loading und verschiedene LLM-Aufgaben gebaut
    Dazu gehören das automatische Auswählen des größten quantisierten Modells abhängig vom verfügbaren Speicher, das Extrahieren des Tokenizers aus gguf und das Einfügen von Prompts. Damit ließen sich wohl einige Python-Abhängigkeiten loswerden
    Derzeit unterstützt es llama.cpp, aber auch das ist ziemlich interessant. Es würde interessieren, ob eine Unterstützung für Grammar Constraints geplant ist
    https://github.com/ShelbyJenkins/llm_client

  • Die Formulierung no dependency im Titel ist nicht besonders klar
    Auf den ersten Blick könnte man an no_std denken, aber tatsächlich ist es nicht no_std, und es scheint auch einige Abhängigkeiten zu geben. Gemeint sein könnte höchstens, dass es ausschließlich Rust-Abhängigkeiten sind

    • Titel sind schwierig. Gemeint war, dass es keine Deep-Learning-Abhängigkeiten wie PyTorch, CUDA oder ONNX gibt und die gesamte Logik in sich geschlossen ist
      Ganz transparent gesagt gibt es 5 grundlegende Rust-Abhängigkeiten; davon sollten chrono und clap per Feature-Flag nur für die Chat-Funktion aktiviert werden. Die übrigen 3 sind Utility-Crates, um etwas mehr Hardware-Leistung herauszuholen: rayon für einfachere Parallelisierung, wide zur Unterstützung von SIMD und memmap2 für Memory-Mapping der Model-Dateien
    • Im README wirkt es so, als würden in requirements.txt PyTorch und mehrere Python-Abhängigkeiten benötigt, und das ist auf der Seite auch die einzige Stelle, an der überhaupt das Wort „dependency“ auftaucht; deshalb ist die Titelwahl ziemlich verwirrend
      Das Projekt selbst scheint einfach den Untertitel „Minimal LLM inference in Rust“ zu verwenden. Im Git-Verlauf sieht es so aus, als sei der Account, der den Beitrag gepostet hat, ein Contributor, aber nicht der Hauptautor; daher wäre eine Erklärung hilfreich, was zero dependencies hier genau bedeutet
    • Ursprünglich hätte vielleicht ein Titel wie „keine Hardware-Abhängigkeit“ oder „keine GPU-Abhängigkeit“ Sinn ergeben
      Leider löscht HN in Titeln oft Wörter ohne besonderen Grund oder erkennbare Transparenz
    • Man fragt sich, ob Rusts cargo inzwischen fast wie npm geworden ist
      Bei 16 Abhängigkeiten ist schwer nachzuvollziehen, wie man von keinen Abhängigkeiten sprechen kann
  • Jemand hatte früher etwas Ähnliches gebaut, war aber von der Performance im Vergleich zu C/C++-Code auf der CPU enttäuscht
    Das heißt auch, dass man wohl nicht wirklich wusste, wie man Rust schnell macht. Benchmarks verschiedener Rust-Implementierungen wären interessant
    Es sieht so aus, als könnte die Implementierung von LLM-Inferenz für ernsthafte Programmierer das neue „Hello, world!“ werden
    https://github.com/gip/yllama.rs

    • Dazu gibt es auch ein ähnliches „Hello, world“-Projekt aus eigener Erfahrung
      https://github.com/crabml/crabml
      Einige SIMD-Instruktionen wurden direkt verwendet, und leistungsmäßig schien es mit llama.cpp mithalten zu können. Entscheidend sind wohl der Einsatz von SIMD bei quantisierter Matrixmultiplikation und die Verwendung einer Busy-Wait-Schleife statt einer Condition Variable bei der Aufteilung der Arbeit zwischen Threads
      Allerdings fehlte die Zeit, weiter an Inferenz für quantisierte Modelle mit Vulkan auf der GPU zu arbeiten, daher gab es seit einiger Zeit keine Updates mehr
  • Interessant ist, dass bereits Dioxus verwendet wird, und ich frage mich, ob WASM ebenfalls in die Roadmap aufgenommen werden könnte.
    Wenn sich ein leichtgewichtiges LLM wie RWKV im Browser ausführen ließe, könnte der Browser ganz neue Funktionskategorien eröffnen, ohne eine SaaS-API aufzurufen.

    • Ich habe in dem Bereich ein wenig herumprobiert.
      https://github.com/maedoc/rwkv.js
      Ich habe mit Emscripten kompiliertes Rwkv.cpp verwendet, aber den Tokenizer-Teil noch nicht richtig gelöst. Trotzdem scheint 1.6B RWKV6 für einen rein offline genutzten Browser durchaus brauchbar zu sein.
      Für normales Chatten reicht die Leistung nicht aus, aber für Anwendungen wie RAG könnte es durchaus genügen.
    • Die Bibliothek selbst dürfte sich mit sehr wenigen Änderungen zu WASM kompilieren lassen.
      Die notwendigen Abhängigkeiten rayon und wide unterstützen WASM direkt, und wenn man den Typ Mmap in transformer.rs durch &[u8] ersetzt, könnte auch memmap2 entfernt werden.
      RWKV hat allerdings eine völlig andere Architektur, daher müsste alles neu implementiert werden, und es wirkt sehr unwahrscheinlich, dass das in die Roadmap aufgenommen wird.
  • Ich frage mich, ob diese Implementierungen alle auf CPU beschränkt sind.
    Es wird gefragt, ob man sich besser nach einer anderen Alternative umsehen sollte, wenn man eine gute GPU hat.

    • Ja. Dieses Projekt läuft auf der CPU, nutzt die GPU also nicht für Berechnungen.
      Wenn du ein Rust-Framework mit GPU-Unterstützung ausprobieren möchtest, lohnt sich ein Blick auf Candle https://github.com/huggingface/candle/tree/main.
    • Alles ist für die CPU implementiert, und zumindest aktuell gibt es überhaupt keine GPU-Beschleunigung.
      Wenn das Ziel der tatsächliche Einsatz ist, wäre selbst bei ausschließlicher CPU-Nutzung eher eine Alternative wie llama.cpp sinnvoll. Dieses Projekt ist eher Lehrmaterial, das zeigt, wie es intern funktioniert, wenn man die komplexen Schichten des Ökosystems entfernt.
      LLMs wirken in ihrer Wirkung fast magisch, sind aus Codesicht aber ziemlich einfach.
    • Je nach GPU kann der Unterschied 10- bis 20-fach sein.
      Auf Rust-Seite gibt es llama.cpp-Wrapper wie mein llm_client, außerdem Candle-basierte Projekte wie mistral.rs und Kalosm.
      Mein Projekt soll ebenfalls eine mistral.rs-Implementierung anbieten, aber der vollständige Umstieg von llama.cpp ist noch nicht geschafft. Eine vollständige Rust-Implementierung hat große Vorteile, etwa schnellere Installationszeiten. Mein aktuelles Crate muss allerdings geklont und gebaut werden; zwar ist das für macOS, Windows und Linux automatisiert, aber die Build-Zeit verlängert sich um etwa eine Minute.
    • CPU stimmt, aber noch wichtiger ist die Speicherbandbreite.
      Eine RTX 3090 hat zum Beispiel fast 1 TB/s Speicherbandbreite. Um das zu erreichen, bräuchte man mindestens 12 Kanäle DDR5, selbst mit der schnellsten Proof-of-Concept-Konfiguration der Welt.
      Wenn man eine dedizierte GPU hat, ist es eine völlig andere Welt, eine Implementierung zu verwenden, die diese nutzt. Dass die LLM-Inferenzwerte von Apple Silicon so beeindruckend sind, liegt auch an der integrierten CPU-GPU-Architektur mit hoher Speicherbandbreite; soweit ich mich erinnere, waren es rund 400 GB/s.
    • Das hängt vom Einzelfall ab. Gute Modelle sind groß und haben hohe Speicheranforderungen.
      Selbst eine 4090 hat aus LLM-Sicht nicht besonders viel Speicher. Eine GPU wird zwar schneller sein, aber bei großen Modellen reicht der Speicher womöglich nicht aus.
  • Ich frage mich, welchen Wert das im Vergleich zu llama.cpp hat.

    • Es lässt sich möglicherweise leichter in andere Rust-Projekte integrieren.
    • Durch den geringeren Funktionsumfang ist die Codebasis sauberer.
  • Stark, und Glückwunsch zur ersten Rust-Bibliothek, aber für ernsthafte lokale Nutzung sind Metal/CUDA-Unterstützung unverzichtbar.

    • Mit CUDA würde man am Zweck dieses Projekts vorbeigehen, daher passt das von Anfang an nicht wirklich.
      Ich bin zwar nicht der Hauptautor, experimentiere aber als Mitwirkender mit wgpu, um ein gewisses Maß an GPU-Beschleunigung zu erreichen. Der Hauptautor möchte die Komplexität begrenzen, daher weiß ich nicht, wie weit das am Ende tatsächlich gehen wird.
  • Die Leidenschaft der Rust-Community, fast alles neu zu schreiben, ist interessant und wirkt auf mich positiv