Ternlight — modelo de embeddings de 7 MB que roda no navegador (WASM)
(ternlight-demo.vercel.app)- 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-L6e faz destilação - Depois de instalar
@ternlight/base, basta importarembedesimilarpara 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,cosineSimesimilarcosineSim(embed('reset my password'), embed('I forgot my password'))→ 0.88similaroferece busca de vizinhos mais próximos em um corpus, com suporte a definirtopK- 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-L6e 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,0ou+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
- Todos os pesos são um de
-
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
- Integra modelo + tokenizador BERT + engine em um único
-
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
embedesimilarde@ternlight/basepara executar busca baseada em significadoimport { 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 passsentence-transformers/all-MiniLM-L6-v2— modelo professor a partir do qual o modelo aluno foi destilado- Licença: MIT
1 comentários
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.
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.
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.
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.
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.
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.
https://news.ycombinator.com/item?id=27016630
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.
É 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.