Ternlight — un modèle d’embedding de 7 Mo exécuté dans le navigateur (WASM)
(ternlight-demo.vercel.app)- Effectue l’embedding de texte et la recherche par similarité directement dans le navigateur, sans appel serveur, pour mettre rapidement en place une recherche sémantique
- Fonctionne uniquement sur CPU, sans GPU ; le package de base fait 7 Mo moteur et poids compris, la version mini 5 Mo
- Intègre le moteur, le modèle et le tokenizer BERT dans un unique fichier
.wasm, sans postinstall ni fetch à l’exécution - Distillé à partir du modèle
all-MiniLM-L6avec un entraînement conscient de la quantification (QAT) de type BitNet b1.58 - Après installation de
@ternlight/base, il suffit d’importerembedetsimilarpour créer un flux de recherche sémantique en 3 lignes de code - Atteint une compression ×30 avec une perte de précision limitée, rendant possibles la confidentialité, l’usage hors ligne et la recherche en edge sans aller-retour réseau
ternlight — des embeddings entièrement dans le navigateur
- Outil d’embedding sémantique fourni sous forme de bundle WebAssembly de 5 à 7 Mo, regroupant moteur + modèle + tokenizer dans un seul fichier pour effectuer la recherche par embeddings sur CPU
- Transforme le texte en embeddings en quelques millisecondes, sans appel serveur
- Propose deux tiers via la même API, à choisir selon le compromis taille/qualité
@ternlight/base— tier qualité (7 Mo wire, environ 5 ms par embedding)@ternlight/mini— tier compact et rapide (5 Mo wire, environ 2,5 ms par embedding)
API principale
- Une seule primitive : chaîne de caractères → renvoie un Float32Array normalisé L2 à 384 dimensions
- Trois fonctions fournies :
embed,cosineSim,similarcosineSim(embed('reset my password'), embed('I forgot my password'))→ 0.88similarprend en charge la recherche des plus proches voisins dans un corpus, avec possibilité de définirtopK- Exemple : une recherche sur « I want my money back » renvoie « Refunds: how to get your money back » (sim 0.70) et « Update your billing address » (sim 0.24)
- Fonctionne avec Node ≥ 18, les navigateurs (via bundler), Cloudflare Workers, Vercel Edge, Deno et Bun, avec routage automatique vers le loader adapté à chaque environnement
Principes de conception
- Distillé depuis
all-MiniLM-L6avec application d’un entraînement conscient de la quantification de style BitNet b1.58 ; trois choix de conception permettent d’atteindre une taille de quelques Mo -
Poids ternaires (Ternary weights)
- Tous les poids valent
-1,0ou+1, ce qui fait que l’inférence se traite par additions et soustractions - Le modèle est entraîné dès le départ comme modèle ternaire afin de préserver la qualité
- Tous les poids valent
-
Bundle unique (One bundle)
- Modèle + tokenizer BERT + moteur intégrés dans un seul
.wasm - Pas d’étape postinstall ni de fetch à l’exécution
- Modèle + tokenizer BERT + moteur intégrés dans un seul
-
Moteur d’inférence SIMD
- Rust écrit à la main, compilé en WASM SIMD
- Les opérations d’addition/soustraction exploitent les instructions vectorielles du CPU
Indicateurs de performance
- Toutes les mesures sont basées sur le build int4 publié (Mac série M, Node/V8)
-
@ternlight/mini
- Taille wire (wasm gzip) : 5,0 Mo, latence (p50) : 2,5 ms
- Débit (monothread) : environ 400 emb/s
- Spearman (vs enseignant) : 0.820, Retrieval (SciFact NDCG@10) : 0.439
- Architecture : 2 couches · d_model=256 · 4 têtes, environ 9,5 M de paramètres
-
@ternlight/base
- Taille wire (wasm gzip) : 7,2 Mo, latence (p50) : 5,1 ms
- Débit (monothread) : environ 195 emb/s
- Spearman (vs enseignant) : 0.844, Retrieval (SciFact NDCG@10) : 0.465
- Architecture : 2 couches · d_model=384 · 6 têtes, environ 15,4 M de paramètres
-
Spécifications communes
- Sortie : normalisation L2 à 384 dimensions
- Entrée maximale : 128 tokens (environ 95 mots)
- Quantification : poids ternaires · embeddings int4
Usages des embeddings on-device
-
Search-as-you-type
- Affiche les résultats avant même que l’utilisateur ait terminé sa saisie, plus rapidement que n’importe quel aller-retour réseau
-
Apps sensibles à la confidentialité
- Les requêtes et documents ne quittent pas l’appareil, sans contrat de traitement de données ni risque de fuite
-
Apps offline-first
- Extensions de navigateur, plugins Obsidian, applications desktop
-
Apps en runtime edge
- Sur Cloudflare Workers, Deno Deploy et Vercel Edge, les embeddings sont placés au même endroit que le handler de requête, sans appel à un service d’inférence séparé
-
Appareils edge et matériel IoT
- Raspberry Pi, ordinateurs monocartes, passerelles industrielles, kiosques
- Les opérations d’addition/soustraction s’exécutent efficacement sur les cœurs ARM, sans GPU ni NPU
-
Sites statiques
- Avec Jekyll, Hugo et Astro, le modèle est déployé avec le bundle, pour une recherche sémantique sans backend
Installation et exemple d’utilisation
- Fourni sous forme d’un seul package npm, utilisable sans étape de téléchargement de modèle ni serveur séparé
- La commande d’installation est la suivante
npm install @ternlight/base - En important
embedetsimilardepuis@ternlight/base, on exécute une recherche sémantiqueimport { embed, similar } from '@ternlight/base'; similar('easy weeknight dinner ideas', recipes, { topK: 3 }); // → ranked matches · ~5 ms · zero network
Open source sous-jacent et licence
- BitNet b1.58 (Ma et al., Microsoft Research, 2024) — recherche architecturale sur l’entraînement de poids ternaires
bitlinear— implémentation de référence PyTorch de BitLinear, utilisée directement à l’entraînement (bitlinear==2.4.6), avec un moteur d’inférence Rust qui reflète tel quel le calcul de propagation avantsentence-transformers/all-MiniLM-L6-v2— modèle enseignant à partir duquel le modèle étudiant est distillé- Licence : MIT
1 commentaires
Avis sur Hacker News
Pour un projet perso, je voulais faire tourner un modèle utile dans le navigateur ; j’ai donc distillé un petit encodeur de phrases à partir de MiniLM, puis appliqué un entraînement avec quantification ternaire consciente
J’ai aussi écrit moi-même le moteur d’inférence et je le déploie en Rust → WASM SIMD
Ce n’est pas un LLM, mais un modèle d’embeddings : quand on lui donne du texte, il produit un vecteur à 384 dimensions, et on juge la pertinence entre deux textes via la similarité cosinus de deux vecteurs. Par exemple, « reset my password » et « I forgot my password » peuvent donner 0,88
Il peut servir à la recherche sémantique, au matching FAQ/intention et au clustering ; comme il s’exécute sur l’appareil, il permet une recherche sémantique rapide dès la saisie, sans dépendre d’une API
La démo recherche dans 2 000 documents React entièrement sur l’appareil : https://ternlight-demo.vercel.app
Sur npm, il y a deux niveaux : @ternlight/base (7 Mo, environ 5 ms par embedding, embeddings plus performants) et @ternlight/mini (5 Mo transférés, environ 2,5 ms par embedding), empaquetés pour Node et le navigateur
Le dépôt contient les détails techniques, la licence MIT et le pipeline d’entraînement : https://github.com/soycaporal/ternlight
Je me demande si les embeddings sur l’appareil sont réellement utiles, et pour quels cas d’usage
Je me demande si cela pourrait aider à trouver crêpe quand l’utilisateur saisit « pancake », sans devoir ajouter explicitement une entrée de dictionnaire « pancake = crêpe »
Si j’ai bien compris, la bibliothèque télécharge 5 Mo une première fois, puis on l’utilise ensuite un peu comme j’utilise Fuse.js aujourd’hui ?
J’aimerais aussi savoir dans quelle mesure elle gère les langues autres que l’anglais, et si on peut l’entraîner avec le wiki des tags OpenStreetMap
Je me demande s’il existe une comparaison avec d’autres modèles d’embeddings ultralégers. Difficile de savoir si le choix de partir de MiniLM-L6 vient du fait que c’est un modèle particulièrement bon dans cette catégorie, puisque la seule métrique fournie est « Retrieval (SciFact NDCG@10) »
Cela dit, je suis assez loin des performances annoncées : dans Firefox sur un i5-4570, je n’obtiens que 35 embeddings par seconde, pas 400. Je soupçonne un basculement vers un chemin sans SIMD, et je compte aussi essayer le binaire Rust natif
C’est cool, mais ce serait bien d’avoir un bouton pour lancer la démo sur la landing page. J’ai été assez surpris d’entendre les ventilateurs s’emballer dès l’ouverture de la page
Ce serait bien d’en faire un plugin Astro ou de méta-framework générique, qui parserait automatiquement tous les fichiers HTML générés et créerait une petite base de données d’embeddings
Côté frontend, on pourrait la charger paresseusement, et peut-être stocker aussi HNSW par chunks pour ne charger que les parties nécessaires à la requête
Par exemple, quelque chose de proche de https://pagefind.app/, mais avec une recherche vectorielle entièrement statique
Plusieurs mois, voire années, ont passé, et si c’est toujours le cas, c’est assez décevant : cela ressemble à un signe que le projet n’a pas les moyens de vraiment aboutir. En plus, je l’avais recommandé comme bon candidat dans une subvention à laquelle j’ai postulé ; eux ont été retenus et moi non
Si quelqu’un connaît une bonne solution dans ce domaine, ou si je me trompe au sujet de SQLite-vec, j’aimerais le savoir. Pour notre SSG, on a plus ou moins décidé de travailler quelques mois sur une autre infrastructure, puis de le construire nous-mêmes si ce n’est toujours pas prêt
Ça pourrait être un ajout vraiment sympa au projet de recherche HNSW avec DuckDB que j’avais vu ici auparavant : https://github.com/jasonjmcghee/portable-hnsw
Le fait que la recherche s’effectue sur des fichiers Parquet hébergés statiquement en utilisant des requêtes HTTP Range est vraiment intéressant
Je pense que ce genre de choses pourrait devenir un écosystème de recherche relativement ouvert et distribué, non contrôlé par les grandes entreprises
https://news.ycombinator.com/item?id=27016630
C’est vraiment cool, et ça pourrait être la pièce manquante d’une chose que je voulais construire depuis un moment
Avec https://github.com/npiesco/absurder-sql, on peut persister tout le corpus source dans IndexedDB/SQLite à l’intérieur du navigateur
Ensuite, comme dans https://weaviate.io/blog/chunking-strategies-for-rag, au lieu de tout indexer à l’avance, on peut utiliser Ternlight pour générer et mettre en cache les embeddings à la demande
Cela permettrait aussi une recherche hybride via Reciprocal Rank Fusion, en combinant FTS5/BM25 de SQLite natif avec la recherche sémantique de Ternlight
Beau travail
C’est présenté comme faisant 7 Mo, mais il existe aussi une version mini à 5 Mo
Il semble que mini utilise en interne des vecteurs de 256 éléments, et non 384, pour réduire l’espace, puis les projette à la fin en 384 pour des raisons de compatibilité
La taille baisse d’un tiers, mais la perte n’est pas linéaire ; même avec un chemin de données plus petit, la perte d’information semble inférieure à un tiers
Super projet
J’avais essayé quelque chose de similaire auparavant : http://sol.quipu-strands.com/
Je voulais charger un modèle d’embeddings dans le navigateur pour ordonner sémantiquement du texte
J’ai récupéré des poids ONNX (MPNet, MiniLM) depuis HuggingFace, généré les embeddings avec Transformers.js, puis utilisé un algorithme de clustering de scikit-learn exécuté dans la page via pyodide. Tout tournait côté client, et j’ai été surpris que ça fonctionne parfaitement
La démo se comporte de façon assez étrange. Par exemple, si l’on cherche « how to use typescript with createContext », les meilleurs résultats ne sont que des entrées typescript, ce qui donne l’impression que la recherche par similarité échoue
Merci. Les modèles locaux apporteront un jour la confidentialité, et je connais déjà un excellent cas d’usage qui convient parfaitement à ce genre de petits modèles d’embeddings : la recherche bon marché et rapide dans une base de données produits
Dans mon cas, le fait de s’appuyer sur le CPU est aussi un avantage
Peut-on précalculer la génération des embeddings qui prend 30 secondes et les envoyer au navigateur ?
Ensuite, l’inférence est rapide et agréable