- lm.rs est un projet qui exécute localement l’inférence de modèles de langage sur CPU en Rust, avec pour objectif une implémentation minimale réalisant l’inférence complète sans bibliothèque de ML
- Inspiré de llama2.c et llm.c de Karpathy, il ne prenait au départ en charge que Google Gemma 2, avant d’être étendu à Llama 3.2 et à la prise en charge d’images avec PHI-3.5
- Une évolution récente a ajouté le traitement par lots, ce qui accélère l’encodage d’images jusqu’à environ 3x, et Llama 3.2 1B tourne à 50 tok/s sur la machine 16 cœurs de l’auteur
- Les modèles préparés sont disponibles sur Hugging Face, et le README recommande d’utiliser Q8_0, en précisant que la quantification Q4_0 est encore en cours d’amélioration
- Les utilisateurs peuvent télécharger directement un modèle et un tokenizer au format LMRS pour compiler immédiatement, ou convertir les fichiers de modèle d’origine de Hugging Face avec export.py et tokenizer.py
Objectifs de lm.rs
- lm.rs est une implémentation d’inférence de modèles de langage locale sur CPU écrite en Rust
- L’objectif est une implémentation minimale réalisant l’inférence complète d’un modèle de langage sur CPU, sans bibliothèque de ML
- Le projet est inspiré de llama2.c et de llm.c
- Le README indique que le code actuel n’est « pas si minimal », et que certaines parties peuvent encore être optimisées et améliorées
- Le projet a aussi servi de point de départ à l’auteur pour découvrir Rust
Modèles pris en charge et extension multimodale
- Au départ, seuls les modèles Google Gemma 2 étaient pris en charge, puis la prise en charge de Llama 3.2 a été ajoutée
- Plus récemment, une option d’utilisation d’images a été ajoutée via PHI-3.5
- Éléments actuellement mis en avant
- prise en charge multimodale via le modèle PHI-3.5-vision
- prise en charge du modèle texte seul PHI-3.5-mini
- Ressources associées
Performances et modèles préparés
- Dernière nouveauté : le traitement par lots a été implémenté, améliorant la vitesse d’encodage d’images jusqu’à environ 3x
- Llama 3.2 1B s’exécute à 50 tok/s sur la machine 16 cœurs de l’auteur
- Les modèles et tokenizers préparés peuvent être téléchargés depuis Hugging Face
- Les mesures de vitesse ont été effectuées sur un AMD Epyc 16 cœurs
- Le README recommande Q8_0 et précise que la quantification Q4_0 est encore en cours d’amélioration
-
Tableau des modèles préparés
- Gemma 2 2B IT Q4_0 : 1.39G, 20 tok/s
- Gemma 2 2B IT Q8_0 : 2.66GB, 24 tok/s
- Gemma 2 9B IT Q4_0 : 4.91GB, 7 tok/s
- Gemma 2 9B IT Q8_0 : 9.53GB, 8 tok/s
- Llama 3.2 1B IT : 4.94GB, 21 tok/s
- Llama 3.2 1B IT Q8_0 : 1.27GB, 50 tok/s
- Llama 3.2 3B IT Q4_0 : 1.71GB, 17 tok/s
- Llama 3.2 3B IT Q8_0 : 3.31GB, 19 tok/s
- PHI 3.5 IT Vision Q8_0 : 4.28GB, 17 tok/s
- PHI 3.5 IT Mini Q8_0 : 3.94GB, 18 tok/s
Flux de conversion des modèles
- En téléchargeant des modèles quantifiés préparés et des tokenizers depuis Hugging Face, il est possible d’ignorer l’étape de conversion
- Pour convertir directement des modèles publiés sur Hugging Face par Google ou Meta, il faut installer des dépendances Python supplémentaires
pip install -r requirements.txt
- Les fichiers .safetensors et config.json sont téléchargés depuis la page du modèle d’origine
- Pour les modèles multimodaux comme PHI3.5 Vision, un fichier config de CLIP est également nécessaire
export.py convertit les poids en bfloat16 au format LMRS
python export.py --files [ordered .safetensor files] --config [model config.json] --save-path [name and path to save] --type [model type (GEMMA/LLAMA/PHI)]
- Pour exporter une version quantifiée, il faut utiliser les options --quantize et --quantize-type
- La taille d’un modèle quantifié en int8 peut être réduite d’environ 9.8G à environ 2.5G selon la taille de groupe
- Les modèles multimodaux doivent inclure l’argument --vision-config
tokenizer.py convertit le modèle de tokenizer au format tokenizer LMRS
python tokenizer.py --model-id [huggingface model_id] --tokenizer-type [type of the tokenizer (GEMMA/LLAMA/PHI)]
Compilation et exécution
- Le code Rust se compile avec
cargo, et le README précise de passer l’option target-cpu
RUSTFLAGS="-C target-cpu=native" cargo build --release --bin chat
- Pour activer les fonctions multimodales, il faut ajouter l’argument --features multimodal
- L’exécution de base se fait en indiquant le fichier de poids du modèle
./target/release/chat --model [model weights file]
- Des arguments supplémentaires permettent d’utiliser le tokenizer, temperature, top-p, show-metrics, etc.
- Les arguments disponibles sont consultables avec --help
- Avec un modèle multimodal, le chemin de l’image se spécifie via l’argument --image
- Pour PHI3.5-vision, le README recommande une temperature à 0
Exécuter le backend WebUI
- Pour lancer le backend destiné à WebUI, il faut compiler avec la fonctionnalité backend
RUSTFLAGS="-C target-cpu=native" cargo build --release --features backend --bin backend
- Le backend multimodal active la fonctionnalité backend-multimodal
- Le backend se lance en indiquant le fichier de poids du modèle
./target/release/backend --model [model weights file]
- --ip et --port permettent de modifier l’IP et le port
- Des options supplémentaires comme temperature peuvent aussi être utilisées
- La compatibilité multimodale s’active avec l’option --multimodal
- Une fois lancé, il est possible de se connecter via l’interface web
État des TODO et licence
- Éléments terminés
- ajout d’autres méthodes d’échantillonnage
- dans les tests des modèles 9B et 27B, le test 9B est terminé, le 27B est indiqué comme trop lent
- parallélisation de la boucle d’attention multi-têtes
- ajout d’indicateurs de performance
- prise en charge de la quantification int8 et int4
- Éléments restants
- fonction de fourniture d’un prompt système
- La licence est MIT
1 commentaires
Avis sur Hacker News
Sur un MacBook M2 64 Go, j’ai essayé de lancer le fichier de 1,2 Go
llama3.2-1b-it-q80.lmrset cela m’a paru assez rapide ; d’après Activity Monitor, il utilisait 1000 % de CPU sur 13 threads.J’ai cloné
lm.rsdans/tmp, compilé avecRUSTFLAGS="-C target-cpu=native" cargo build --release --bin chat, puis téléchargétokenizer.binetllama3.2-1b-it-q80.lmrsdepuis Hugging Face, avant de lancer./target/release/chat --model llama3.2-1b-it-q80.lmrs../target/release/chat --model llama3.2-1b-it-q80.lmrs --show-metricspour voir combien de tokens par seconde on obtient.Seule une partie a été conservée à cause du formatage, mais cela prenait la forme d’une longue suite continue de mots aléatoires.
L’article est très bien écrit, et je pourrais utiliser une partie du code source en cours pour expliquer comment les transformers fonctionnent réellement.
Le code est plus concret et plus détaillé que les schémas de têtes d’attention. Cela dit, si la bibliothèque écrit directement sur
stdout, elle risque de perturber la sortie d’applications, par exemple un éditeur de texte qui fournit une vérification de style ; il vaudrait mieux écrire dans un tampon de chaînes d’une instance de journalisation associée à l’objetlm.rs.Je vois aussi, dans le lecteur de modèle, un passage qui utilise
unsafepour imposer l’alignement des données ; je me demande s’il serait possible de faire cela sansunsafe, sans trop forcer.Cela permettrait par exemple d’afficher les logs dans une GUI.
J’ai développé pas mal d’outils Rust pour le chargement de modèles et diverses tâches LLM.
Ils permettent notamment de choisir automatiquement le plus grand modèle quantifié selon la mémoire disponible, d’extraire le tokenizer depuis un
gguf, ou encore d’insérer un prompt. Cela pourrait aider à supprimer certaines dépendances Python.Pour l’instant, c’est destiné à la prise en charge de
llama.cpp, mais ceci est aussi assez intéressant. Je me demande aussi s’il est prévu de prendre en charge les contraintes grammaticales (grammar).https://github.com/ShelbyJenkins/llm_client
L’expression no dependency dans le titre manque de clarté.
À première vue, j’ai pensé qu’il pouvait s’agir de
no_std, mais en réalité ce n’est pasno_stdet il semble bien y avoir quelques dépendances. Peut-être que cela signifie simplement qu’elles sont toutes en Rust.En toute transparence, il y a 5 dépendances Rust de base, dont
chronoetclap, qu’il faudrait effectivement mettre derrière des feature flags pour la fonction de chat. Les 3 autres sont des crates utilitaires destinées à tirer un peu plus de performance du matériel :rayonpour faciliter la parallélisation,widepour aider avec le SIMD, etmemmap2pour le memory mapping des fichiers de modèle.requirements.txtnécessite PyTorch et plusieurs dépendances Python ; comme c’est aussi le seul endroit de la page où le mot « dependency » apparaît, la formulation du titre est assez déroutante.Le projet lui-même semble simplement utiliser le sous-titre « Minimal LLM inference in Rust ». D’après l’historique Git, le compte qui a posté cet article est contributeur mais ne semble pas être l’auteur principal ; une explication de ce que signifie exactement zero dependencies serait utile.
Malheureusement, HN supprime parfois des mots des titres sans raison ni transparence particulières.
cargode Rust est maintenant presque devenu commenpm.Je ne vois pas comment on peut parler de zéro dépendance quand il y en a 16.
J’avais créé quelque chose de similaire il y a quelque temps, mais les performances étaient décevantes par rapport à du code C/C++ exécuté sur CPU.
Cela veut aussi dire que je ne savais pas vraiment comment rendre Rust rapide. Ce serait bien d’avoir des benchmarks de plusieurs implémentations Rust.
L’implémentation de l’inférence LLM pourrait devenir le nouveau « Hello, world! » des programmeurs sérieux.
https://github.com/gip/yllama.rs
https://github.com/crabml/crabml
J’ai utilisé directement certaines instructions SIMD, et les performances semblaient pouvoir s’aligner sur celles de
llama.cpp. Le point clé semble être l’utilisation de SIMD pour la multiplication de matrices quantifiées, ainsi que le recours à une boucle d’attente active plutôt qu’à des variables de condition pour répartir le travail entre threads.En revanche, je n’ai pas eu le temps de continuer à travailler sur l’inférence de modèles quantifiés sur GPU avec Vulkan, donc le projet n’a pas été mis à jour depuis un moment.
Le fait que Dioxus soit déjà utilisé est intéressant, et je me demande si WASM pourrait aussi entrer dans la roadmap
Si l’on pouvait faire tourner dans le navigateur un LLM léger comme RWKV, cela ouvrirait une nouvelle catégorie de fonctionnalités côté navigateur, sans avoir à appeler une API SaaS
https://github.com/maedoc/rwkv.js
J’ai utilisé
Rwkv.cppcompilé avec Emscripten, mais la partie tokenizer n’est pas encore vraiment résolue. Malgré tout, RWKV6 1.6B semble pouvoir être suffisamment exploitable pour un usage exclusivement dans un navigateur hors ligneIl n’a pas les capacités nécessaires pour du chat généraliste, mais pourrait être tout à fait suffisant pour des usages comme le RAG
Les dépendances obligatoires
rayonetwideprennent directement en charge WASM, et si l’on remplace le typeMmapdetransformer.rspar&[u8], on peut aussi supprimermemmap2En revanche, RWKV a une architecture complètement différente, il faudrait donc tout réimplémenter, et il paraît très peu probable que cela entre dans la roadmap
Je me demande si toutes ces implémentations sont limitées au CPU
La question est de savoir si, avec un bon GPU, il vaut mieux chercher une autre solution
Si vous voulez essayer un framework Rust prenant en charge le GPU, Candle https://github.com/huggingface/candle/tree/main vaut le détour
Si l’objectif est vraiment d’exécuter des modèles, même en n’utilisant que le CPU, il vaut mieux prendre une alternative, à savoir
llama.cpp. Ce projet ressemble davantage à un support pédagogique montrant comment les choses fonctionnent en interne lorsqu’on retire les couches complexes de l’écosystèmeLes LLM ont l’air magiques par leurs effets, mais du point de vue du code, ils sont assez simples
Côté Rust, il existe des wrappers de
llama.cppcomme monllm_client, ainsi que des projets basés sur Candle commemistral.rset KalosmMon projet vise aussi à proposer une implémentation
mistral.rs, mais je n’ai pas encore complètement migré depuisllama.cpp. Une implémentation entièrement en Rust présente de gros avantages, notamment des temps d’installation plus rapides. Pour l’instant, mon crate doit être cloné et compilé ; c’est automatisé sur macOS, Windows et Linux, mais cela ajoute environ une minute de compilationPar exemple, une RTX 3090 dispose de près de 1 To/s de bande passante mémoire. Pour atteindre cela, il faudrait au minimum 12 canaux de la DDR5 la plus rapide au monde, au stade de preuve de concept
Si l’on dispose d’un GPU dédié, utiliser une implémentation qui en tire parti est un tout autre monde. Les chiffres d’inférence LLM d’Apple Silicon sont impressionnants aussi parce que l’architecture de mémoire unifiée CPU-GPU offre une forte bande passante ; de mémoire, c’était autour de 400 Go/s
Même une 4090 n’a pas tant de mémoire que cela à l’échelle des LLM. Le GPU sera certes plus rapide, mais il est possible de ne pas pouvoir y charger de grands modèles
Je me demande quelle valeur cela apporte par rapport à
llama.cppC’est super, et félicitations aussi pour avoir créé une première bibliothèque Rust, mais pour un usage local sérieux, la prise en charge de Metal/CUDA est indispensable
Cela dit, sans être l’auteur principal mais en tant que contributeur, je mène des expérimentations pour obtenir un certain niveau d’accélération GPU avec
wgpu. L’auteur principal veut garder la complexité sous contrôle, donc je ne sais pas jusqu’où cela ira réellementL’enthousiasme de la communauté Rust à presque tout réécrire est intéressant et appréciable