1 pontos por GN⁺ 2024-10-12 | 1 comentários | Compartilhar no WhatsApp
  • 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

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

 
GN⁺ 2024-10-12
Comentários no Hacker News
  • Ao testar o llama3.2-1b-it-q80.lmrs de 1,2 GB em um MacBook M2 64GB, pareceu bem rápido, e no Activity Monitor ele usou 1000% de CPU em 13 threads
    Clonou lm.rs em /tmp, compilou com RUSTFLAGS="-C target-cpu=native" cargo build --release --bin chat, depois baixou tokenizer.bin e llama3.2-1b-it-q80.lmrs no Hugging Face e executou com ./target/release/chat --model llama3.2-1b-it-q80.lmrs

    • Perguntou se daria para executar com ./target/release/chat --model llama3.2-1b-it-q80.lmrs --show-metrics para verificar quantos tokens por segundo ele entrega
    • Ao inserir um prompt simples de conversa em francês, no começo parecia responder, mas logo desandou em uma saída interminável e incompreensível de lixo
      Parte foi removida por causa do formato, mas era uma longa sequência contínua de palavras aleatórias
    • Ficou curioso sobre como entender o quanto isso é inteligente em comparação com o ChatGPT atual
  • 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 objeto lm.rs
    També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 sem unsafe

    • Em vez de um buffer de string, seria melhor chamar um callback do usuário
      Assim 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 gguf e inserir prompts. Isso talvez ajude a remover algumas dependências de Python
    Atualmente é para suporte a llama.cpp, mas ainda assim é bem interessante. Também ficou curioso se há planos de suporte a grammar constraints
    https://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_std e parece haver algumas dependências. Talvez queira dizer apenas que são todas dependências Rust

    • Dar título é difícil. O que queria dizer era que não há dependências de deep learning como PyTorch, CUDA, ONNX, e que toda a lógica é autocontida
      Sendo transparente, há 5 dependências Rust básicas, e entre elas chrono e clap deveriam 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: rayon para facilitar paralelização, wide para ajudar com SIMD e memmap2 para memory mapping do arquivo do modelo
    • Pelo README, parece que requirements.txt exige 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 confusa
      O 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
    • Originalmente talvez fizesse sentido se o título fosse algo como “sem dependência de hardware” ou “sem dependência de GPU”
      Infelizmente o HN costuma remover palavras dos títulos sem muita razão ou transparência
    • Dá a impressão de que o cargo do Rust agora virou quase um npm
      Nã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

    • Eu também já fiz algo parecido nessa linha de “Hello, world”
      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 threads
      Mas 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

    • Já mexi um pouco com isso
      https://github.com/maedoc/rwkv.js
      Usei Rwkv.cpp compilado 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 navegador
      Ele não tem capacidade suficiente para chat geral, mas pode ser bem adequado para usos como RAG
    • A biblioteca em si provavelmente permitiria compilação para WASM com pouquíssimas mudanças
      As dependências obrigatórias rayon e wide já oferecem suporte direto a WASM, e se o tipo Mmap de transformer.rs fosse trocado por &[u8], também daria para remover memmap2
      Poré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

    • Sim. Este projeto roda na CPU, então não usa a GPU para computação
      Se você quiser experimentar um framework Rust com suporte a GPU, vale dar uma olhada no Candle https://github.com/huggingface/candle/tree/main
    • Está tudo implementado em CPU e, pelo menos por enquanto, não há nenhuma aceleração por GPU
      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 ecossistema
      LLMs parecem magia em termos de efeito, mas do ponto de vista do código são bem simples
    • Dependendo da GPU, a diferença pode ser de 10 a 20 vezes
      No lado Rust, existem wrappers de llama.cpp como meu llm_client, e projetos baseados em Candle como mistral.rs e Kalosm
      Meu projeto também pretende oferecer uma implementação de mistral.rs, mas ainda não consegui migrar totalmente de llama.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 minuto
    • É CPU, sim, mas mais importante ainda é a largura de banda da memória
      Por 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
    • Depende do caso. Modelos bons são grandes e têm exigência de memória alta
      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.cpp

    • Pode ser mais fácil de integrar com outros projetos Rust
    • Tem menos recursos, então a base de código é mais limpa
  • Ficou 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

    • Usar CUDA iria contra o propósito deste projeto, então desde o início não combina com a proposta
      Dito isso, embora eu não seja o autor principal, como contribuidor estou fazendo experimentos com wgpu para 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