- lm.rs es un proyecto para ejecutar inferencia de modelos de lenguaje locales en CPU con Rust, y busca ser una implementación de código mínima que realiza toda la inferencia sin bibliotecas de ML
- Está inspirado en llama2.c y llm.c de Karpathy; al principio solo soportaba Google Gemma 2, pero se amplió para soportar Llama 3.2 y entrada de imágenes con PHI-3.5
- Con los cambios más recientes se implementó el procesamiento por lotes, lo que aceleró la codificación de imágenes hasta aproximadamente 3 veces; Llama 3.2 1B corre a 50 tok/s en la máquina de 16 núcleos del autor
- Los modelos preparados se pueden descargar desde Hugging Face; el README recomienda usar Q8_0 e indica que la cuantización Q4_0 todavía está en mejora
- Los usuarios pueden descargar modelos y tokenizadores en formato LMRS para compilar de inmediato, o convertir los archivos de modelos originales de Hugging Face con export.py y tokenizer.py para ejecutarlos
Qué busca lm.rs
- lm.rs es una implementación de inferencia de modelos de lenguaje locales en CPU escrita en Rust
- Su objetivo es ser una implementación de código mínima que realice la inferencia completa de modelos de lenguaje en CPU sin bibliotecas de ML
- Está inspirado en llama2.c y llm.c de Karpathy
- El README indica que el código actual “no es tan mínimo”, y que parte del código tiene margen para optimización y mejoras
- El proyecto también fue una oportunidad para que el autor probara Rust por primera vez
Modelos soportados y expansión multimodal
- Al principio solo soportaba el modelo Gemma 2 de Google, pero luego se agregó soporte para modelos Llama 3.2
- Recientemente se añadió la opción de usar imágenes mediante PHI-3.5
- Elementos de soporte destacados actualmente
- Soporte multimodal mediante el modelo PHI-3.5-vision
- Soporte para el modelo solo de texto PHI-3.5-mini
- Recursos relacionados
Rendimiento y modelos preparados
- Como novedad más reciente, se implementó el procesamiento por lotes, mejorando la velocidad de codificación de imágenes hasta aproximadamente 3 veces
- Llama 3.2 1B corre a 50 tok/s en la máquina de 16 núcleos del autor
- Los modelos y tokenizadores preparados se pueden descargar desde Hugging Face
- Las mediciones de velocidad se realizaron en un AMD Epyc de 16 núcleos
- El README recomienda usar Q8_0 e indica que la cuantización Q4_0 todavía está en mejora
-
Tabla de modelos preparados
- 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
Flujo de conversión de modelos
- Si descargas modelos cuantizados y tokenizadores preparados desde Hugging Face, puedes omitir el proceso de conversión
- Para convertir directamente los modelos publicados por Google o Meta en Hugging Face, se deben instalar dependencias adicionales de Python
pip install -r requirements.txt
- Se descargan y usan los archivos .safetensors y config.json desde la página del modelo original
- Para modelos multimodales como PHI3.5 Vision también se necesita el archivo config de CLIP
export.py convierte pesos bfloat16 al formato 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)]
- Para exportar una versión cuantizada, se usan las banderas --quantize y --quantize-type
- El tamaño de un modelo cuantizado int8 puede reducirse de unos 9.8G a unos 2.5G, según el tamaño del grupo
- Los modelos multimodales deben incluir el argumento --vision-config
tokenizer.py convierte el modelo del tokenizador al formato de tokenizador LMRS
python tokenizer.py --model-id [huggingface model_id] --tokenizer-type [type of the tokenizer (GEMMA/LLAMA/PHI)]
Compilación y ejecución
- El código Rust se compila con
cargo, y el README especifica pasar la bandera target-cpu
RUSTFLAGS="-C target-cpu=native" cargo build --release --bin chat
- Para activar la función multimodal, se agrega el argumento --features multimodal
- La ejecución básica se realiza especificando el archivo de pesos del modelo
./target/release/chat --model [model weights file]
- Se pueden usar argumentos adicionales como tokenizer, temperature, top-p y show-metrics
- Los argumentos disponibles se consultan con --help
- En modelos multimodales se especifica la ruta de la imagen con el argumento --image
- Al usar PHI3.5-vision, el README recomienda temperature 0
Ejecución del backend de WebUI
- Para ejecutar el backend de WebUI, se compila con la función backend
RUSTFLAGS="-C target-cpu=native" cargo build --release --features backend --bin backend
- El backend multimodal activa la función backend-multimodal
- El backend se ejecuta especificando el archivo de pesos del modelo
./target/release/backend --model [model weights file]
- Se pueden cambiar la IP y el puerto con --ip y --port
- También se pueden usar banderas adicionales como temperature
- Para compatibilidad multimodal se usa la bandera --multimodal
- Después de ejecutarlo, se puede conectar a la interfaz web
Estado del TODO y licencia
- Elementos completados
- Agregar otros métodos de muestreo
- De los puntos de prueba para modelos 9B y 27B, se completó la prueba de 9B; 27B aparece marcado como probablemente demasiado lento
- Paralelización del bucle de atención multi-head
- Agregar métricas de rendimiento
- Soporte para cuantización int8 e int4
- Elementos pendientes
- Función para proporcionar system prompts
- La licencia es MIT
1 comentarios
Opiniones de Hacker News
Al probar el
llama3.2-1b-it-q80.lmrsde 1.2 GB en una MacBook M2 de 64 GB, se sintió bastante rápido y, según Activity Monitor, usó 1000% de CPU en 13 hilos.Cloné
lm.rsen/tmp, lo compilé conRUSTFLAGS="-C target-cpu=native" cargo build --release --bin chat, luego bajétokenizer.binyllama3.2-1b-it-q80.lmrsdesde Hugging Face y lo ejecuté con./target/release/chat --model llama3.2-1b-it-q80.lmrs../target/release/chat --model llama3.2-1b-it-q80.lmrs --show-metricspara ver cuántos tokens por segundo produce.Por formato solo se dejó una parte, pero era una larga secuencia continua de palabras aleatorias.
El artículo está muy bien escrito, y podría usar parte del código fuente en clase para explicar cómo funcionan realmente los transformers.
El código es más concreto y detallado que los diagramas de cabezales de atención. Sin embargo, si la librería imprime directamente en
stdout, puede arruinar la salida de aplicaciones, por ejemplo cuando un editor de texto ofrece revisión de estilo; sería mejor escribir en un búfer de cadenas de una instancia de logging asociada al objetolm.rs.También se ve una parte donde se usa
unsafeen el lector de modelos para forzar la alineación de datos, y da curiosidad si sería posible hacerlo sin forzar tanto, sinunsafe.Así se podría manejar, por ejemplo, mostrando los logs en una GUI.
Ya hay varias herramientas en Rust para tareas con LLM y carga de modelos.
Incluyen funciones como seleccionar automáticamente el modelo cuantizado más grande según la memoria disponible, extraer el tokenizador desde
ggufo insertar prompts. Con esto quizá se puedan eliminar algunas dependencias de Python.Actualmente está orientado a soportar
llama.cpp, pero esto también resulta bastante interesante. También preguntan si hay planes de soporte para restricciones gramaticales (grammar).https://github.com/ShelbyJenkins/llm_client
La expresión no dependency del título no es muy clara.
Al verla por primera vez, pensé que podría ser
no_std, pero en realidad no esno_stdy parece tener algunas dependencias. Quizá quiera decir que todas son dependencias de Rust.Para ser transparentes, hay 5 dependencias básicas de Rust, y entre ellas
chronoyclapdeberían gestionarse con feature flags para la función de chat. Las otras 3 son crates utilitarios para sacar un poco más de rendimiento del hardware:rayonpara facilitar la paralelización,widepara ayudar con SIMD ymemmap2para mapear archivos de modelo en memoria.requirements.txtparece requerir PyTorch y varias dependencias de Python, y ese es el único lugar de la página donde aparece la palabra “dependency”, así que la expresión del título resulta bastante confusa.El proyecto en sí parece usar simplemente el subtítulo “Minimal LLM inference in Rust”. Al mirar el historial de Git, la cuenta que publicó esto parece ser contribuidora, pero no la autora principal; ayudaría que explicaran qué significa exactamente zero dependencies.
Por desgracia, HN a veces elimina palabras de los títulos sin mucha razón ni transparencia.
cargode Rust ya se volvió casi comonpm.No entiendo cómo puede llamarse sin dependencias si tiene 16 dependencias.
Hace tiempo hice algo parecido, pero el rendimiento era decepcionante comparado con código C/C++ corriendo en CPU.
Eso también significa que no sabía bien cómo hacer que Rust fuera rápido. Sería bueno tener benchmarks de varias implementaciones en Rust.
La implementación de inferencia de LLM podría convertirse en el nuevo “Hello, world!” para programadores serios.
https://github.com/gip/yllama.rs
https://github.com/crabml/crabml
Usé directamente algunas instrucciones SIMD, y parecía que el rendimiento podía igualar al de
llama.cpp. La clave parece estar en usar SIMD para la multiplicación de matrices cuantizadas y, al dividir el trabajo entre hilos, usar un bucle de espera activa en vez de variables de condición.Sin embargo, no he podido actualizarlo por un tiempo porque no tuve tiempo para seguir trabajando en la inferencia de modelos cuantizados en GPU con Vulkan.
Es interesante que ya use Dioxus, y me pregunto si WASM también podría entrar en la hoja de ruta.
Si fuera posible correr un LLM ligero como RWKV en el navegador, el navegador podría abrir una nueva categoría de funciones sin llamar a una API SaaS.
https://github.com/maedoc/rwkv.js
Usé
Rwkv.cppcompilado con Emscripten, pero todavía no resolví bien la parte del tokenizador. Aun así, 1.6B RWKV6 parece lo suficientemente usable para un navegador exclusivamente offline.Para chat general le falta capacidad, pero para usos como RAG podría ser bastante suficiente.
Las dependencias obligatorias
rayonywideya soportan WASM, y si se cambia el tipoMmapdetransformer.rspor&[u8], también se puede eliminarmemmap2.Sin embargo, RWKV tiene una arquitectura totalmente distinta, así que habría que implementarlo todo de nuevo, y parece muy poco probable que entre en la hoja de ruta.
Me pregunto si todas estas implementaciones están limitadas a CPU.
Preguntan si, en caso de tener una buena GPU, lo correcto sería buscar otra alternativa.
Si quieres probar un framework de Rust con soporte para GPU, vale la pena mirar Candle https://github.com/huggingface/candle/tree/main.
Si el objetivo es ejecutarlo en la práctica, aun usando solo CPU probablemente convenga más usar una alternativa, es decir,
llama.cpp. Este proyecto se parece más a material educativo que muestra cómo funciona por dentro cuando se eliminan las capas complejas del ecosistema.Los LLM parecen magia por sus resultados, pero desde el punto de vista del código son bastante simples.
En el lado de Rust hay wrappers de
llama.cppcomo millm_client, y proyectos basados en Candle comomistral.rsy Kalosm.Mi proyecto también intenta ofrecer una implementación con
mistral.rs, pero todavía no he migrado por completo desdellama.cpp. Una implementación completa en Rust tiene grandes ventajas, como acelerar los tiempos de instalación. En este momento mi crate requiere clonar y compilar, así que aunque está automatizado en macOS, Windows y Linux, agrega alrededor de 1 minuto al tiempo de compilación.Por ejemplo, una RTX 3090 tiene casi 1 TB/s de ancho de banda de memoria. Para igualar eso habría que usar al menos 12 canales de la DDR5 de prueba de concepto más rápida del planeta.
Si tienes una GPU dedicada, usar una implementación que la aproveche es un mundo completamente distinto. La razón por la que las cifras de inferencia de LLM en Apple Silicon son impresionantes también es su arquitectura de memoria unificada CPU-GPU de alto ancho de banda; si no recuerdo mal, era de unos 400 GB/s.
Incluso una 4090 no tiene tanta memoria según los estándares de los LLM. La GPU será más rápida, pero es posible que no puedas cargar modelos grandes.
Me pregunto qué valor tiene esto en comparación con
llama.cpp.Es genial, y felicitaciones por crear tu primera biblioteca en Rust, pero para un uso local serio es indispensable el soporte de Metal/CUDA.
Dicho eso, aunque no soy el autor principal sino colaborador, estoy experimentando con obtener cierta aceleración por GPU mediante
wgpu. El autor principal quiere mantener la complejidad bajo control, así que no sé hasta dónde llegará realmente.La pasión de la comunidad de Rust por reescribir casi todo me parece interesante y se agradece.