- lm.rs é um projeto para executar inferência de modelos de linguagem localmente em CPU com Rust, com o objetivo de implementar a inferência completa com código mínimo, sem bibliotecas de ML
- Foi inspirado em llama2.c e llm.c de Karpathy, e começou com suporte apenas ao Google Gemma 2, mas depois se expandiu para incluir Llama 3.2 e entrada de imagem com PHI-3.5
- Como mudança mais recente, foi implementado o processamento em lote, acelerando a codificação de imagens em até cerca de 3x, e o Llama 3.2 1B roda a 50 tok/s na máquina de 16 núcleos do autor
- Modelos prontos podem ser baixados no Hugging Face, e o README recomenda usar Q8_0, informando que a quantização Q4_0 ainda está sendo melhorada
- O usuário pode baixar um modelo e tokenizer no formato LMRS e compilar imediatamente, ou converter os arquivos originais de modelo do Hugging Face com export.py e tokenizer.py para executar
O que o lm.rs busca
- lm.rs é uma implementação de inferência de modelo de linguagem local em CPU escrita em Rust
- O objetivo é uma implementação mínima de código que realize a inferência completa de modelos de linguagem na CPU sem bibliotecas de ML
- Foi inspirado em llama2.c e llm.c de Karpathy
- O README afirma que o código atual “não é tão mínimo assim” e que algumas partes ainda têm espaço para otimização e melhorias
- O projeto também serviu como uma forma de o autor experimentar Rust pela primeira vez
Modelos suportados e expansão multimodal
- No início, havia suporte apenas ao modelo Google Gemma 2, mas depois foi adicionado suporte aos modelos Llama 3.2
- Recentemente, foi adicionada a opção de usar imagens por meio do PHI-3.5
- Itens de suporte atualmente destacados
- Suporte multimodal via modelo PHI-3.5-vision
- Suporte ao modelo somente de texto PHI-3.5-mini
- Recursos relacionados
Desempenho e modelos prontos
- Como novidade recente, foi implementado o processamento em lote, melhorando a velocidade de codificação de imagens em até cerca de 3x
- O Llama 3.2 1B roda a 50 tok/s na máquina de 16 núcleos do autor
- Modelos e tokenizers prontos podem ser obtidos no Hugging Face
- As medições de velocidade foram feitas em um AMD Epyc de 16 núcleos
- O README recomenda o uso de Q8_0 e informa que a quantização Q4_0 ainda está sendo aprimorada
-
Tabela de modelos prontos
- 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
Fluxo de conversão de modelos
- Se você baixar do Hugging Face os modelos quantizados prontos e o tokenizer, pode pular o processo de conversão
- Para converter diretamente modelos publicados no Hugging Face pelo Google ou pela Meta, é preciso instalar dependências Python adicionais
pip install -r requirements.txt
- Os arquivos .safetensors e config.json são baixados e usados a partir da página do modelo original
- Modelos multimodais como o PHI3.5 Vision também exigem o arquivo config do CLIP
export.py converte pesos em bfloat16 para o 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 uma versão quantizada, use as flags --quantize e --quantize-type
- O tamanho de um modelo quantizado em int8 pode cair de cerca de 9.8G para cerca de 2.5G, dependendo do tamanho do grupo
- Modelos multimodais devem incluir o argumento --vision-config
tokenizer.py converte o modelo de tokenizer para o formato de tokenizer LMRS
python tokenizer.py --model-id [huggingface model_id] --tokenizer-type [type of the tokenizer (GEMMA/LLAMA/PHI)]
Compilação e execução
- O código Rust é compilado com
cargo, e o README indica explicitamente passar a flag target-cpu
RUSTFLAGS="-C target-cpu=native" cargo build --release --bin chat
- Para ativar a funcionalidade multimodal, adicione o argumento --features multimodal
- A execução básica é feita especificando o arquivo de pesos do modelo
./target/release/chat --model [model weights file]
- Argumentos adicionais incluem tokenizer, temperature, top-p, show-metrics e outros
- Os argumentos disponíveis podem ser consultados com --help
- Em modelos multimodais, o caminho da imagem é informado com o argumento --image
- Ao usar PHI3.5-vision, o README recomenda temperature 0
Execução do backend da WebUI
- Para rodar o backend da WebUI, compile com a feature backend
RUSTFLAGS="-C target-cpu=native" cargo build --release --features backend --bin backend
- O backend multimodal ativa a feature backend-multimodal
- O backend é executado especificando o arquivo de pesos do modelo
./target/release/backend --model [model weights file]
- --ip e --port permitem alterar IP e porta
- Flags adicionais como temperature também podem ser usadas
- Para compatibilidade multimodal, use a flag --multimodal
- Depois de executar, é possível conectar-se pela interface web
Estado do TODO e licença
- Itens concluídos
- Adição de outros métodos de sampling
- Entre os testes com modelos 9B e 27B, o teste com 9B foi concluído, e o de 27B foi marcado como provavelmente lento demais
- Paralelização do loop de atenção multi-head
- Adição de métricas de desempenho
- Suporte a quantização int8 e int4
- Itens restantes
- Recurso para fornecer system prompt
- A licença é MIT
1 comentários
Comentários no Hacker News
Ao testar o
llama3.2-1b-it-q80.lmrsde 1,2 GB em um MacBook M2 64GB, pareceu bem rápido, e no Activity Monitor ele usou 1000% de CPU em 13 threadsClonou
lm.rsem/tmp, compilou comRUSTFLAGS="-C target-cpu=native" cargo build --release --bin chat, depois baixoutokenizer.binellama3.2-1b-it-q80.lmrsno Hugging Face e executou com./target/release/chat --model llama3.2-1b-it-q80.lmrs./target/release/chat --model llama3.2-1b-it-q80.lmrs --show-metricspara verificar quantos tokens por segundo ele entregaParte foi removida por causa do formato, mas era uma longa sequência contínua de palavras aleatórias
O texto está muito bem escrito, e talvez dê para usar parte do código-fonte ao explicar em aula como transformers realmente funcionam
O código é mais concreto e detalhado do que diagramas de attention heads. Ainda assim, se a biblioteca escrever diretamente em
stdout, isso pode atrapalhar a saída de aplicações como editores de texto que oferecem verificação de estilo, então seria melhor escrever no buffer de string de uma instância de logging conectada ao objetolm.rsTambém notou uma parte em que
unsafeé usado no leitor do modelo para forçar alinhamento de dados, e ficou curioso se isso seria possível semunsafeAssim daria para tratar casos como exibir logs em uma GUI
Já criou bastante coisa para carregamento de modelos e várias ferramentas Rust para tarefas com LLM
Há recursos como selecionar automaticamente o maior modelo quantizado com base na memória disponível, extrair o tokenizer de
ggufe inserir prompts. Isso talvez ajude a remover algumas dependências de PythonAtualmente é para suporte a
llama.cpp, mas ainda assim é bem interessante. Também ficou curioso se há planos de suporte a grammar constraintshttps://github.com/ShelbyJenkins/llm_client
A expressão no dependency no título é pouco clara
Ao ver pela primeira vez, pensou que talvez significasse
no_std, mas na prática não éno_stde parece haver algumas dependências. Talvez queira dizer apenas que são todas dependências RustSendo transparente, há 5 dependências Rust básicas, e entre elas
chronoeclapdeveriam mesmo ficar atrás de feature flags para a funcionalidade de chat. As outras 3 são crates utilitárias para extrair um pouco mais de desempenho do hardware:rayonpara facilitar paralelização,widepara ajudar com SIMD ememmap2para memory mapping do arquivo do modelorequirements.txtexige PyTorch e várias dependências Python, e esse também é o único lugar na página em que a palavra “dependency” aparece, então a formulação do título é bem confusaO próprio projeto parece usar apenas o subtítulo “Minimal LLM inference in Rust”. Pelo histórico do Git, a conta que publicou este post parece ser de um colaborador, mas não do autor principal, então seria útil explicar exatamente o que zero dependencies quer dizer
Infelizmente o HN costuma remover palavras dos títulos sem muita razão ou transparência
cargodo Rust agora virou quase umnpmNão entende como dá para dizer sem dependências com 16 dependências
Já tinha feito algo parecido antes, mas o desempenho no CPU ficou aquém em comparação com código C/C++ rodando em CPU
Isso também quer dizer que ele não sabia direito como tornar Rust rápido. Seria bom ter benchmarks entre várias implementações Rust
Implementar inferência de LLM talvez vire o novo “Hello, world!” para programadores sérios
https://github.com/gip/yllama.rs
https://github.com/crabml/crabml
Usei algumas instruções SIMD diretamente, e parecia possível alcançar o desempenho do
llama.cpp. O ponto-chave parece ser usar SIMD na multiplicação de matrizes quantizadas e usar um loop de espera ocupada em vez de variável de condição ao dividir trabalho entre threadsMas não tive tempo de continuar trabalhando em inferência de modelos quantizados com Vulkan na GPU, então faz tempo que não atualizo isso
É interessante ver que eles já usam Dioxus, e fico curioso se WASM poderia entrar no roadmap também
Se fosse possível rodar um LLM leve como RWKV no navegador, o browser poderia abrir uma nova categoria de funcionalidades sem precisar chamar uma API SaaS
https://github.com/maedoc/rwkv.js
Usei
Rwkv.cppcompilado com Emscripten, mas ainda não consegui resolver direito a parte do tokenizer. Mesmo assim, o RWKV6 1.6B parece suficientemente utilizável para uso offline só no navegadorEle não tem capacidade suficiente para chat geral, mas pode ser bem adequado para usos como RAG
As dependências obrigatórias
rayonewidejá oferecem suporte direto a WASM, e se o tipoMmapdetransformer.rsfosse trocado por&[u8], também daria para removermemmap2Porém, RWKV tem uma arquitetura completamente diferente, então seria preciso reimplementar tudo do zero, e a chance de isso entrar no roadmap parece muito baixa
Fico curioso se essas implementações são todas limitadas à CPU
A pergunta é se, tendo uma boa GPU, o certo seria procurar outra alternativa
Se você quiser experimentar um framework Rust com suporte a GPU, vale dar uma olhada no Candle https://github.com/huggingface/candle/tree/main
Se a ideia for usar isso de fato em execução real, mesmo ficando só na CPU, seria melhor optar por uma alternativa como
llama.cpp. Este projeto está mais próximo de um material didático que mostra como as coisas funcionam por dentro quando se removem as camadas complexas do ecossistemaLLMs parecem magia em termos de efeito, mas do ponto de vista do código são bem simples
No lado Rust, existem wrappers de
llama.cppcomo meullm_client, e projetos baseados em Candle comomistral.rse KalosmMeu projeto também pretende oferecer uma implementação de
mistral.rs, mas ainda não consegui migrar totalmente dellama.cpp. Uma implementação 100% Rust tem vantagens grandes, como reduzir o tempo de instalação. Hoje meu crate ainda precisa ser clonado e compilado, então, embora haja automação para macOS, Windows e Linux, o tempo de build aumenta em cerca de 1 minutoPor exemplo, uma RTX 3090 tem quase 1 TB/s de largura de banda de memória. Para alcançar isso, seria preciso no mínimo algo como 12 canais de DDR5, mesmo em um nível de prova de conceito entre os mais rápidos do planeta
Se você tiver uma GPU dedicada, usar uma implementação que aproveite isso é um mundo completamente diferente. Os números impressionantes de inferência de LLM no Apple Silicon também se devem à arquitetura de memória unificada de alta largura de banda entre CPU e GPU; se bem me lembro, era algo em torno de 400 GB/s
Até uma 4090 não tem tanta memória assim pelos padrões de LLM. A GPU certamente será mais rápida, mas há uma boa chance de não conseguir carregar modelos maiores
Fico curioso sobre qual seria o valor disso em comparação com
llama.cppFicou muito bom, e parabéns por ter criado sua primeira biblioteca Rust, mas para uso local sério é indispensável ter suporte a Metal/CUDA
Dito isso, embora eu não seja o autor principal, como contribuidor estou fazendo experimentos com
wgpupara obter algum nível de aceleração por GPU. Como o autor principal quer manter a complexidade sob controle, não sei até onde isso realmente vai avançarÉ interessante e até dá uma sensação de gratidão ver o entusiasmo da comunidade Rust em reescrever quase tudo