24 pontos por GN⁺ 8 일 전 | 1 comentários | Compartilhar no WhatsApp
  • Realiza embeddings de texto e busca por similaridade dentro do navegador, sem chamadas ao servidor, permitindo montar rapidamente uma busca semântica
  • Funciona apenas com CPU, sem GPU; o pacote base tem 7 MB somando engine e pesos, e o mini tem 5 MB
  • Integra engine, modelo e tokenizador BERT em um único arquivo .wasm, sem necessidade de postinstall nem fetch em tempo de execução
  • Aplica treinamento com ciência de quantização (QAT) no estilo BitNet b1.58 ao modelo all-MiniLM-L6 e faz destilação
  • Depois de instalar @ternlight/base, basta importar embed e similar para criar um fluxo de busca semântica com 3 linhas de código
  • Alcança compressão de 30× com apenas uma pequena perda de precisão, viabilizando privacidade, uso offline e busca na edge sem idas e voltas pela rede

ternlight — embeddings resolvidos dentro do navegador

  • Ferramenta de embeddings semânticos fornecida como um bundle WebAssembly de 5–7 MB, reunindo engine + modelo + tokenizador em um único arquivo para realizar busca por embeddings na CPU
  • Gera embeddings de texto em milissegundos e não chama servidor
  • Oferece dois tiers com a mesma API, para escolher conforme o trade-off entre tamanho e qualidade
    • @ternlight/base — tier de qualidade (7 MB no wire, cerca de 5 ms por embed)
    • @ternlight/mini — tier pequeno e rápido (5 MB no wire, cerca de 2,5 ms por embed)

API principal

  • Um único primitivo: string → retorna um Float32Array L2-normalizado de 384 dimensões
  • Fornece três funções: embed, cosineSim e similar
    • cosineSim(embed('reset my password'), embed('I forgot my password')) → 0.88
    • similar oferece busca de vizinhos mais próximos em um corpus, com suporte a definir topK
      • Ex.: ao buscar 'I want my money back', retorna 'Refunds: how to get your money back' (sim 0,70) e 'Update your billing address' (sim 0,24)
  • Funciona em Node ≥ 18, navegadores (via bundler), Cloudflare Workers, Vercel Edge, Deno e Bun, com roteamento automático para o loader adequado a cada ambiente

Princípios de design

  • Destilado a partir do all-MiniLM-L6 e com aplicação de treinamento com ciência de quantização no estilo BitNet b1.58, alcançando tamanho de poucos MB por meio de três escolhas de design
  • Pesos ternários (Ternary weights)

    • Todos os pesos são um de -1, 0 ou +1, e a inferência é processada com somas e subtrações
    • O modelo é treinado desde o início como modelo ternário, preservando a qualidade
  • Bundle único (One bundle)

    • Integra modelo + tokenizador BERT + engine em um único .wasm
    • Não há etapa de postinstall nem fetch em tempo de execução
  • Engine de inferência SIMD

    • Rust escrito à mão é compilado para WASM SIMD
    • Operações de soma/subtração aproveitam instruções vetoriais da CPU

Métricas de desempenho

  • Todos os números foram medidos com base no build int4 lançado (Mac série M, Node/V8)
  • @ternlight/mini

    • Wire size (gzip wasm): 5,0 MB, latência (p50): 2,5 ms
    • Throughput (thread única): cerca de 400 emb/s
    • Spearman (vs. professor): 0,820, Retrieval (SciFact NDCG@10): 0,439
    • Arquitetura: 2 camadas · d_model=256 · 4 heads, cerca de 9,5 M parâmetros
  • @ternlight/base

    • Wire size (gzip wasm): 7,2 MB, latência (p50): 5,1 ms
    • Throughput (thread única): cerca de 195 emb/s
    • Spearman (vs. professor): 0,844, Retrieval (SciFact NDCG@10): 0,465
    • Arquitetura: 2 camadas · d_model=384 · 6 heads, cerca de 15,4 M parâmetros
  • Especificações comuns

    • Saída: 384 dimensões L2-normalizadas
    • Entrada máxima: 128 tokens (cerca de 95 palavras)
    • Quantização: pesos ternários · embeddings int4

Usos de embeddings on-device

  • Search-as-you-type

    • Mostra resultados antes de o usuário terminar de digitar, mais rápido do que qualquer ida e volta pela rede
  • Apps sensíveis à privacidade

    • Consultas e documentos não saem do dispositivo, sem contratos de processamento de dados nem risco de vazamento
  • Apps offline-first

    • Extensões de navegador, plugins do Obsidian, apps desktop
  • Apps em runtimes de edge

    • Em Cloudflare Workers, Deno Deploy e Vercel Edge, os embeddings ficam colocados no mesmo local do handler de requisições, dispensando chamadas a um serviço de inferência separado
  • Dispositivos de edge e hardware IoT

    • Raspberry Pi, computadores de placa única, gateways industriais, quiosques
    • Operações de soma/subtração rodam com eficiência em núcleos ARM, sem necessidade de GPU ou NPU
  • Sites estáticos

    • Em Jekyll, Hugo e Astro, o modelo é distribuído junto com o bundle, permitindo busca semântica sem backend

Instalação e exemplo de uso

  • Fornecido como um único pacote npm, utilizável sem etapa separada de download de modelo nem servidor
  • O comando de instalação é:
    npm install @ternlight/base  
    
  • Importe embed e similar de @ternlight/base para executar busca baseada em significado
    import { embed, similar } from '@ternlight/base';  
    
    similar('easy weeknight dinner ideas', recipes, { topK: 3 });  
    // → ranked matches · ~5 ms · zero network  
    

Open source de base e licença

  • BitNet b1.58 (Ma et al., Microsoft Research, 2024) — pesquisa de arquitetura para treinamento com pesos ternários
  • bitlinear — implementação de referência em PyTorch do BitLinear, usada diretamente no treinamento (bitlinear==2.4.6), com a engine de inferência em Rust refletindo fielmente as operações de forward pass
  • sentence-transformers/all-MiniLM-L6-v2 — modelo professor a partir do qual o modelo aluno foi destilado
  • Licença: MIT

1 comentários

 
GN⁺ 8 일 전
Comentários no Hacker News
  • Eu queria rodar um modelo útil no navegador como projeto de hobby, então destilei um pequeno codificador de sentenças a partir do MiniLM e apliquei treinamento com reconhecimento de quantização ternária.
    Também escrevi o mecanismo de inferência por conta própria e o distribuí em Rust → WASM SIMD.
    Não é um LLM, mas um modelo de embeddings: ao inserir um texto, ele gera um vetor de 384 dimensões, e a relevância entre textos é determinada pela similaridade de cosseno entre dois vetores. Por exemplo, "reset my password" e "I forgot my password" saem com algo como 0,88.
    Pode ser usado para busca semântica, correspondência de FAQ/intenção e clustering; por rodar no próprio dispositivo, permite busca semântica rápida assim que a entrada é fornecida, sem depender de APIs.
    A demo pesquisa 2 mil documentos do React inteiramente no dispositivo: https://ternlight-demo.vercel.app
    No npm há dois níveis: @ternlight/base (7 MB, cerca de 5 ms por embedding, embeddings com melhor desempenho) e @ternlight/mini (5 MB de transferência, cerca de 2,5 ms por embedding), empacotados para Node e navegador.
    O repositório inclui detalhes técnicos, licença MIT e pipeline de treinamento: https://github.com/soycaporal/ternlight
    Tenho curiosidade se embeddings no dispositivo são realmente úteis e quais casos de uso existem.

    • Tenho um dicionário que mapeia palavras para tags do OpenStreetMap, como em https://codeberg.org/cartes/web/src/branch/master/components...
      Fico curioso se isso ajudaria a fazer com que, quando o usuário digite "pancake", ele encontre crêpe sem eu precisar escrever explicitamente uma entrada de dicionário "pancake = crêpe".
      Se entendi corretamente, também queria saber se a estrutura é a biblioteca baixar 5 MB uma vez no início e, depois disso, ser usada mais ou menos como hoje uso Fuse.js.
      Também gostaria de saber quão bem ela lida com idiomas além do inglês e se daria para treiná-la com a wiki de tags do OpenStreetMap.
    • Tenho muito interesse em colocar uma busca semântica simples em um app desktop nativo.
      Fico curioso se há alguma comparação com outros modelos de embeddings ultrapequenos. É difícil saber se o MiniLM-L6 foi o ponto de partida por ser um modelo particularmente bom nessa categoria, já que a única métrica fornecida é "Retrieval (SciFact NDCG@10)".
      Ainda assim, há uma diferença considerável em relação ao desempenho alegado: no Firefox em um i5-4570, obtenho apenas 35 embeddings por segundo, não 400. Suspeito que possa estar caindo para um caminho sem SIMD, e pretendo testar também o binário Rust nativo.
    • Acabei de gerar embeddings para toda a documentação do django e para a base de conhecimento interna, e agora consigo pesquisar instantaneamente nas duas fontes.
  • Legal, mas seria bom colocar um botão para iniciar a demo na landing page. Fiquei bem surpreso ao abrir a página e ouvir a ventoinha girando feito louca.

    • Concordo. Ao mesmo tempo, isso me deu uma certa nostalgia de uma época em que era possível saber intimamente o que estava acontecendo só pelo som do computador.
    • Me assustei quando a ventoinha começou a girar. Mas, para ser justo, a torradeira também me assusta de vez em quando.
    • Aproveitamento extremo de ciclos de CPU; quem foi que disse que só GPU era especial?
  • Seria bom transformar isso em um plugin para Astro ou para um metaframework genérico, que analisasse automaticamente todos os arquivos HTML gerados e criasse um pequeno banco de dados de embeddings.
    No front-end, ele poderia ser carregado sob demanda, e talvez o HNSW também pudesse ser armazenado em chunks para carregar só as partes necessárias à consulta de busca.
    Por exemplo, algo parecido com https://pagefind.app/, mas oferecendo busca vetorial totalmente estática.

    • No nosso gerador de sites estáticos, queríamos usar sqlite-vec, mas, da última vez que verifiquei, HNSW não estava implementado ou o suporte a busca vetorial no navegador não era bom. Acho que ainda estava fazendo varredura completa da tabela.
      Como meses, talvez anos, se passaram e continua assim, isso me pareceu um sinal meio decepcionante de falta de capacidade para concluir o projeto direito. Pior: recomendei esse projeto como um bom candidato em uma bolsa à qual me candidatei; eles foram selecionados e eu não.
      Se alguém souber de uma boa solução nessa área, ou se eu estiver errado sobre SQLite-vec, gostaria que me dissesse. No nosso SSG, praticamente decidimos trabalhar alguns meses em outra infraestrutura e, se isso ainda não estiver pronto, vamos fazer nós mesmos.
  • Isso poderia ser uma adição bem interessante a um projeto de busca DuckDB HNSW que vi aqui antes: https://github.com/jasonjmcghee/portable-hnsw
    É realmente interessante que a busca aconteça usando requisições HTTP de intervalo sobre arquivos Parquet hospedados estaticamente.
    Acho que coisas assim podem evoluir para um ecossistema de busca relativamente aberto e distribuído, não controlado por grandes empresas.

    • Em uma ideia parecida, também poderia ser interessante usar um banco SQLite em um host estático com requisições HTTP de intervalo e WASM.
      https://news.ycombinator.com/item?id=27016630
    • Ideia excelente. Gosto muito de requisições de intervalo e de formatos que o cliente consegue percorrer em hospedagem estática.
  • Isso é muito legal e pode ser a peça que faltava para algo que eu queria criar antes.
    Com https://github.com/npiesco/absurder-sql, dá para persistir todo o corpus original dentro do navegador em IndexedDB/SQLite.
    Depois, como em https://weaviate.io/blog/chunking-strategies-for-rag, em vez de indexar tudo antecipadamente, dá para usar o Ternlight para gerar e armazenar embeddings em cache sob demanda.
    Isso também permitiria busca híbrida, combinando o FTS5/BM25 do SQLite nativo com a busca semântica do Ternlight por Reciprocal Rank Fusion.

  • Muito bem feito.
    Ele é promovido como tendo 7 MB, mas também há uma versão mini de 5 MB.
    Pelo visto, a mini usa internamente um vetor de 256 elementos em vez de 384 para reduzir espaço e, no fim, projeta para 384 por compatibilidade.
    O tamanho cai em um terço, mas a perda não é linear; mesmo usando um caminho de dados menor, a perda de informação parece ser menor que um terço.

  • Projeto bacana.
    Já tentei algo parecido antes: http://sol.quipu-strands.com/
    Eu queria carregar um modelo de embeddings no navegador e ordenar textos semanticamente.
    Peguei pesos ONNX (MPNet, MiniLM) do HuggingFace, gerei embeddings com Transformers.js e depois usei clusterizadores do scikit-learn rodando em pyodide dentro da página. Tudo rodava do lado do cliente, e fiquei surpreso por funcionar perfeitamente.

  • A demo se comporta de forma bem estranha. Por exemplo, ao buscar "how to use typescript with createContext", os principais resultados são apenas itens de typescript, então parece que a busca por similaridade falhou.

  • Obrigado. Modelos locais um dia trarão privacidade, e já conheço um ótimo caso de uso que combina muito bem com modelos pequenos de embeddings como este: busca barata e rápida em bancos de dados de produtos.
    No meu caso, depender da CPU também é uma vantagem.

    • Legal. Se houver uma forma de apoiar, ou casos de uso específicos que deveriam ser tratados no roadmap, seria bom saber.
  • É possível gerar previamente os embeddings, que levam 30 segundos, e enviá-los para o navegador?
    Depois disso, a inferência é rápida e boa.

    • Sim. Dá para rodar a indexação uma única vez no lado do servidor e enviar apenas os embeddings para o front-end.