WEB3DEV

Cover image for Rust e Infraestrutura LLM de IA: Abrangendo o Poder do Desempenho
Isabela Curado Nehme
Isabela Curado Nehme

Posted on

Rust e Infraestrutura LLM de IA: Abrangendo o Poder do Desempenho

https://miro.medium.com/v2/resize:fit:720/format:webp/1*Ku_ViySEe92pcte17KgAEQ.png

Imagem do autor

Construir uma infraestrutura LLM (Large Language Models ou grandes modelos de linguagem) ou de IA em Rust pode oferecer vários benefícios, apesar do domínio do Python no espaço de IA.

  1. Desempenho: o Rust é conhecido por seu alto desempenho e controle de baixo nível, o que pode ser crucial para a construção de sistemas de IA em grande escala. Os modelos de linguagem, especialmente os modelos de aprendizado profundo, podem ser computacionalmente intensivos. O desempenho do Rust pode levar a melhorias significativas de velocidade em comparação com o Python, tornando-o mais adequado para lidar com eficiência com tarefas computacionalmente caras.
  2. Segurança da memória: as regras rígidas do compilador e o modelo de propriedade do Rust garantem a segurança da memória, evitando bugs comuns, como desreferências de ponteiro nulo e corridas de dados. Isso pode tornar os sistemas de IA baseados em Rust mais confiáveis ​​e menos propensos a travamentos, o que é particularmente importante para modelos de linguagem de longa duração ou aplicações críticas de IA.
  3. Simultaneidade: o suporte integrado do Rust para simultaneidade e threads leves pode levar à utilização eficiente de processadores multicore. Isso pode ser valioso ao implementar o processamento paralelo para treinar grandes modelos de linguagem ou lidar com múltiplas solicitações de inferência simultaneamente.

Sim, vale lembrar que o extenso ecossistema do Python, as bibliotecas bem estabelecidas (por exemplo, TensorFlow, PyTorch) e a facilidade de uso fizeram dele a escolha certa para muitos projetos de IA. Mas, se você tiver um senso de exploração e definir seus requisitos específicos e Rust estiver no topo, esta pode ser uma leitura relevante para você.

Vamos tentar responder à pergunta: o Rust tem uma biblioteca completa, como o Python tem a langchain para trabalhar, com LLMs?

OBS: pode valer a pena exercitar alguns dos blocos de construção por trás da langchain em Python. Para isso, eu criei a langchain-llm-katas, experimente.

Blocos de construção de biblioteca de cadeia

Se nos distanciarmos por um minuto e ignorarmos os fluxos de trabalho de alto nível, como agentes, kits de ferramentas e casos de uso, estaremos olhando para a infraestrutura comum abaixo de todas aquelas que permitem a existência dessas construções de alto nível:

  • LLM — carregar, chamar LLMs e suportar diferentes modelos, que precisam de:
  • Incorporação e tokenização — transformar texto em incorporações, onde o texto é carregado com:
  • Carregadores (loaders) - transformar vários formatos de documento em texto simples e digerível por LLM, que precisa ser pós-processado por:
  • Divisores (splitters) — para criar partes legíveis e úteis do texto original, para trabalhar com os limites de token do LLM e serem armazenados em:
  • Bancos de dados vetoriais - que são usados ​​para formular prompts que são enviados aos LLMs e também para alimentar:
  • Memória — que é feita para suportar contexto e sessões com LLMs. Mas também, temos a infraestrutura para:
  • Modelos — que estruturam uma solicitação de um LLM, que pode conter uma chamada para:
  • Ferramentas - que são uma coleção de ferramentas do mundo real, como uma calculadora ou um navegador para o LLM automatizar por meio da leitura de um prompt estruturado

Então, como é tudo isso atualmente em Rust?

Infraestrutura e blocos de construção

LLM e transformadores

llm

llm é um ecossistema de bibliotecas Rust para trabalhar com grandes modelos de linguagem. Ele foi desenvolvido com base na biblioteca GGML, rápida e eficiente para aprendizado de máquina. É uma biblioteca sólida, expansiva e estável para trabalhar com modelos, a ponto de ser a única biblioteca a ser sempre escolhida, e é bom que não haja muitas alternativas.

Veja como executar inferência usando apenas o crate llm :

let model = llm::load_dynamic(
    Some(model_architecture),
    &model_path,
    tokenizer_source,
    Default::default(),
    llm::load_progress_callback_stdout,
)
.unwrap_or_else(|err| {
    panic!("Falha ao carregar o modelo {model_architecture} do {model_path:?}: {err}")
});
let mut session = model.start_session(Default::default());
let res = session.infer::<Infallible>(
    model.as_ref(),
    &mut rand::thread_rng(),
    &llm::InferenceRequest {
        prompt: prompt.into(),
        parameters: &llm::InferenceParameters::default(),
        play_back_previous_tokens: false,
        maximum_token_count: None,
    },
    // Requisito de saída
    &mut Default::default(),
    |r| match r {
        llm::InferenceResponse::PromptToken(t) | llm::InferenceResponse::InferredToken(t) => {
            print!("{t}");
            std::io::stdout().flush().unwrap();
            Ok(llm::InferenceFeedback::Continue)
        }
        _ => Ok(llm::InferenceFeedback::Continue),
    },
);
Enter fullscreen mode Exit fullscreen mode

rust-bert

Provavelmente a biblioteca mais versátil e produtiva que existe para Rust para o uso de modelos de transformadores, inspirada na biblioteca de transformadores da huggingface. Este é uma opção única para modelos de transformadores locais para diferentes tarefas, desde a tradução até a incorporação.

  • Muitos casos de uso e exemplos.
  • Muito ampla, assim como a biblioteca e transformadores original.
  • Você pode fazer muitas coisas apenas usando esta biblioteca e talvez não precise de mais nada.

Em termos de casos de uso, aqui está um exemplo de análise de sentimentos:

let sentiment_classifier = SentimentModel::new(Default::default())?;

let input = [
    "Provavelmente meu filme favorito de todos os tempos, uma história de abnegação, sacrifício e dedicação para uma causa nobre, mas não é enfadonho ou chato." ,
    "Este filme tentou ser muitas coisas ao mesmo tempo: sátira política pungente, sucesso de bilheteria de Hollywood, comédia romântica sentimental, promoção de valores familiares ..." , 
    "Se você gosta de risadas originais e dolorosas, você vai gostar deste filme. Se você é jovem ou velho, então você vai adorar esse filme, caramba, até minha mãe gostou dele." ,
];
let output = sentiment_classifier.predict(&input);
Enter fullscreen mode Exit fullscreen mode

E incorporações:

let model = SentenceEmbeddingsBuilder::remote(
        SentenceEmbeddingsModelType::AllMiniLmL12V2
    ).create_model()?;
let sentences = [
    "esta é uma frase de exemplo" ,
    "cada frase é convertida"
];
let output = model.encode(&sentences)?;
Enter fullscreen mode Exit fullscreen mode

E muitos outros casos de uso.

Incorporações e Tokenização

Você pode fazer incorporações com o seguinte:

  • rust-bert
  • llm

tiktoken

Esta biblioteca é construída com base na biblioteca tiktoken do OpenAI Rust e a estende um pouco. Deve ser uma boa escolha para suas necessidades de tokenização.

use tiktoken_rs::p50k_base;
let bpe = p50k_base().unwrap();
let tokens = bpe.encode_with_special_tokens(
  "Esta é uma frase    com espaços."
);
println!("Conta do token: {}", tokens.len());
Enter fullscreen mode Exit fullscreen mode

Carregadores

No momento em que este artigo foi escrito, não existia nenhum carregador unificado como o unstructured, que possa carregar e converter documentos sem se preocupar tanto com a implementação do provedor específico. Alguns deles podem precisar de um pouco de esforço para colocar o conteúdo em um formato plano semelhante a um documento LLM (texto simples, páginas), por exemplo, fazendo um loop e extraindo planilhas de arquivos Excel.

No entanto, aqui está um mapeamento sensato do formato de arquivo para uma biblioteca Rust apropriada:

Divisores

https://github.com/benbrandt/text-splitter

A única biblioteca prática o suficiente para divisão. Parece estar adotando uma abordagem de “dividir corretamente” (split properly), onde não é necessário escolher se deseja dividir por novas linhas, caracteres, recursivamente ou tokens. Ela descerá e usará um método apropriado para maximizar o tamanho das partes.

Prompts

No momento em que este artigo foi escrito, não havia um consenso geral e nenhuma biblioteca que unificasse o conceito de modelo como a langchain fez. No entanto, pegar uma biblioteca de modelos e construir em cima dela é uma ótima maneira de implementar prompts.

O Rust tem algumas bibliotecas de modelos fantásticas que se adequam à modelagem:

  • Handlebars — para modelos padrão, familiares e de lógica mínima.
  • Tera — para quem está familiarizado com jinja2.
  • Liquid – para quem está familiarizado com liquid.

Na maioria das vezes, o handlebars é a melhor aposta, pois será familiar ao público em geral.

Bancos de dados vetoriais

Atualmente, não existe uma interface unificada para bancos de dados vetoriais que forneça um empacotador (wrapper) geral para muitos provedores. Pesquise alguns projetos de código aberto sobre LLMs. Você encontrará reimplementações das mesmas interfaces repetidamente, o que é uma pena, e ainda assim, nenhuma biblioteca fornece uma interface genérica.

Dada uma interface tão simplista, há um bom ROI (Return Of Investment ou retorno de investimento) na construção de uma biblioteca desse gênero. Deveria ser, basicamente:

  • add_documents
  • similarity_search

Para sua informação: muitos bancos de dados de vetores individuais são implementados em Rust, tanto comerciais quanto de código aberto. Qdrant é provavelmente o de código aberto mais popular.

Memória

Existem alguns projetos que implementam memória ou histórico. A maioria deles implementa um serviço que armazena índices e históricos de pesquisa para você e também pode ser usado como referência sobre como fazer seu fluxo de trabalho em Rust lidando com IA.

Alguns interessantes são:

  • memex - parte do spyglass, tem uma boa leitura de código e faz armazenamento de documentos e pesquisa semântica para projetos LLM.
  • indexify – memória LLM de longo prazo.
  • motorhead — recuperação de memória/informações.

Ferramentas

No momento em que este artigo foi escrito, nenhuma biblioteca de ferramentas/plugins era dedicada apenas a esse propósito. Ou seja, bibliotecas que fornecem uma interface sólida para:

  • Declarar uma ferramenta;
  • Capacidades;
  • Executar uma ferramenta (com segurança ou não);
  • Estruturar uma solicitação e resposta de ferramenta.

No entanto, essas implementações podem ser encontradas nas poucas implementações de cadeia que o Rust possui atualmente, que são discutidas abaixo.

Cadeias

llmchain-rs

Uma biblioteca de cadeia de pequeno escopo. Eu diria 30% do que a langchain-py tem.

  • Primeiros tempos para este projeto;
  • Boa quantidade de exemplos;
  • Interfaces genéricas, mas focadas no Databend;
  • Boa cobertura de teste (é um desafio testar a infra LLM em qualquer caso).

Vejamos algumas das abstrações:

Incorporação

pub trait Embedding: Send + Sync {
    async fn embed_query(&self, input: &str) -> Result<Vec<f32>>;
    async fn embed_documents(&self, inputs: &Documents) -> Result<Vec<Vec<f32>>>;
}
Enter fullscreen mode Exit fullscreen mode

LLM

pub trait LLM: Send + Sync {
    async fn embedding(&self, inputs: Vec<String>) -> Result<EmbeddingResult>;
    async fn generate(&self, input: &str) -> Result<GenerateResult>;
    async fn chat(&self, _input: Vec<String>) -> Result<Vec<ChatResult>> {
        unimplemented!("")
    }
}
Enter fullscreen mode Exit fullscreen mode
  • As integrações atualmente são: OpenAI, Azure-OpenAI e Databend, então provavelmente a mistura de dados do Databend com a IA foi o gatilho para tudo isso.

Documento

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Document {
    pub path: String,
    pub content: String,
    pub content_md5: String,
}
impl Document {
    pub fn create(path: &str, content: &str) -> Self {
        Document {
            path: path.to_string(),
            content: content.to_string(),
            content_md5: format!("{:x}", md5::compute(content)),
        }
    }
    pub fn tokens(&self) -> usize {
        chat_tokens(&self.content).unwrap().len()
    }
    pub fn size(&self) -> usize {
        self.content.len()
    }
}
#[derive(Debug)]
pub struct Documents {
    documents: RwLock<Vec<Document>>,
}
Enter fullscreen mode Exit fullscreen mode

Prompt

pub trait Prompt: Send + Sync {
    fn template(&self) -> String;
    fn variables(&self) -> Vec<String>;
    fn format(&self, input_variables: HashMap<&str, &str>) -> Result<String>;
}
Enter fullscreen mode Exit fullscreen mode
  • Substituição simples de variáveis ​​apenas com base em texto.
  • Nível único, sem prompts avançados dependentes ou parciais.

Armazenamento de vetores

#[async_trait::async_trait]
pub trait VectorStore: Send + Sync {
    async fn init(&self) -> Result<()>;
    async fn add_documents(&self, inputs: &Documents) -> Result<Vec<String>>;
    async fn similarity_search(&self, query: &str, k: usize) -> Result<Vec<Document>>;
}

Enter fullscreen mode Exit fullscreen mode
  • Suporte para databend apenas como provedor.
  • Falta de MMR para pesquisa (como em todas as interfaces que vi até agora).
  • Abstração de salvar/carregar em falta (mas será que isso pertence a uma característica?).

No Geral

  • Base de código relativamente sólida e boas abstrações.
  • Divisores (sem abstração ou estratégia para escolher, mas este é o caso de todas as implementações de cadeia em Rust que vi até agora).
  • Memória (apenas nos primeiros tempos, parece).

cadeia llm

Uma biblioteca de cadeia mais popular, dividida em crates do Rust, bons idiomas de Rust e estrutura. Não é uma cobertura completa como a langchain-py tem. Talvez 30%, e ainda nos primeiros tempos aqui, como acontece com todas as outras bibliotecas.

Vejamos as abstrações aqui:

Incorporação

#[async_trait]
pub trait Embeddings {
    type Error: Send + Debug + Error + EmbeddingsError;
    async fn embed_texts(&self, texts: Vec<String>) -> Result<Vec<Vec<f32>>, Self::Error>;
    async fn embed_query(&self, query: String) -> Result<Vec<f32>, Self::Error>;
}
Enter fullscreen mode Exit fullscreen mode

Armazenamento de vetores

#[async_trait]
pub trait VectorStore<E, M = EmptyMetadata>
where
    E: Embeddings,
    M: serde::Serialize + serde::de::DeserializeOwned,
{
    type Error: Debug + Error + VectorStoreError;
    async fn add_texts(&self, texts: Vec<String>) -> Result<Vec<String>, Self::Error>;
    async fn add_documents(&self, documents: Vec<Document<M>>) -> Result<Vec<String>, Self::Error>;
    async fn similarity_search(
        &self,
        query: String,
        limit: u32,
    ) -> Result<Vec<Document<M>>, Self::Error>;
}
Enter fullscreen mode Exit fullscreen mode

Divisores

Parece que um divisor é um tokenizador (Tokenizer):

pub trait Tokenizer {
    fn tokenize_str(&self, doc: &str) -> Result<TokenCollection, TokenizerError>;
             fn to_string(&self, tokens: TokenCollection) -> Result<String, TokenizerError>;
    fn split_text(
        &self,
        doc: &str,
        max_tokens_per_chunk: usize,
        chunk_overlap: usize,
    ) -> Result<Vec<String>, TokenizerError>;
}
Enter fullscreen mode Exit fullscreen mode

Prompt

Prompt é uma construção que permite operar com uma abstração de mensagens de Chat ou Text. A modelagem é poderosa e baseada em tera.

Documento

#[derive(Debug)]
pub struct Document<M = EmptyMetadata>
where
    M: serde::Serialize + serde::de::DeserializeOwned,
{
    pub page_content: String,
    pub metadata: Option<M>,
}
Enter fullscreen mode Exit fullscreen mode

Mais semelhante do que diferente em comparação com o documento da langchain-py.

Armazenamento de documento (Carregador?)

#[async_trait]
pub trait DocumentStore<T, M>
where
    T: Send + Sync,
    M: Serialize + DeserializeOwned + Send + Sync,
{
    type Error: std::fmt::Debug + std::error::Error + DocumentStoreError;
async fn get(&self, id: &T) -> Result<Option<Document<M>>, Self::Error>;
    async fn next_id(&self) -> Result<T, Self::Error>;
    async fn insert(&mut self, documents: &HashMap<T, Document<M>>) -> Result<(), Self::Error>;
}
Enter fullscreen mode Exit fullscreen mode

No geral

  • Boa separação entre recursos e crates.
  • Parece compatível com o Rust.
  • Falta divisores e carregadores.
  • Falta integrações em geral e, especialmente, armazenamento de vetores.

SmartGPT

Esta não é uma biblioteca nem uma infraestrutura, mas os componentes internos estão bem construídos e podem ser de boa utilidade para uma biblioteca de cadeia. Vamos dar uma olhada nas abstrações:

LLM

pub trait LLMModel : Send + Sync {
    async fn get_response(&self, messages: &[Message], max_tokens: Option<u16>, temperature: Option<f32>) -> Result<String, Box<dyn Error>>;
    async fn get_base_embed(&self, text: &str) -> Result<Vec<f32>, Box<dyn Error>>;
    fn get_token_count(&self, text: &[Message]) -> Result<usize, Box<dyn Error>>;
    fn get_token_limit(&self) -> usize;
    fn get_tokens_from_text(&self, text: &str) -> Result<Vec<String>, Box<dyn Error>>;
}
Enter fullscreen mode Exit fullscreen mode

Alguns métodos foram removidos porque eram implementados por padrão.

Mensagem

#[derive(Clone, Debug)]
pub enum Message {
    User(String),
    Assistant(String),
    System(String)
}
Enter fullscreen mode Exit fullscreen mode

Modelando uma mensagem em um padrão User-AI-System (Sistema-IA-usuário).

Sistema de memória (histórico?)

pub trait MemorySystem : Send + Sync {
    async fn store_memory(&mut self, llm: &LLM, memory: &str) -> Result<(), Box<dyn Error>>;
async fn get_memory_pool(&mut self, llm: &LLM, memory: &str, min_count: usize) -> Result<Vec<RelevantMemory>, Box<dyn Error>>;
    async fn get_memories(
    }
…
Enter fullscreen mode Exit fullscreen mode

Uma boa interface de memória.

Comando (ferramenta?)

pub trait CommandImpl : Send + Sync {
    async fn invoke(&self, ctx: &mut CommandContext, args: ScriptValue) -> Result<CommandResult, Box<dyn Error>>;
fn box_clone(&self) -> Box<dyn CommandImpl>;
}
Enter fullscreen mode Exit fullscreen mode

No geral

  • Este projeto possui uma coleção de ferramentas relativamente rica e pode ser o melhor começo para uma biblioteca genérica que implementa ferramentas.
  • A base de código é muito pragmática porque quase tudo está realmente em uso.
  • Onde a funcionalidade não era necessária - ela não existe (naturalmente), e é aqui que falta na base de código, em comparação com a langchain: divisores, carregadores, modelos de prompt, armazenamentos de vetores e uma sensação geral de vários provedores.
  • Usá-la como uma biblioteca langchain de uso geral seria estranho, a menos que você atingisse os casos de uso que este projeto já resolve. O que eu acho que não será suficiente rápido.

Obrigado por ler.

Este artigo foi escrito por Dotan Nahum e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Top comments (0)