Loading
La table d'embeddings pèse 31 254 528 octets. Une phrase de douze mots en utilise environ 20 Ko. HTTP Range fait que c'est la seule partie qui circule.
Ouvrir l'instrumentL'essentiel d'un petit BERT n'est pas le modèle. C'est le dictionnaire.
Les blocs transformeur de bert-mini — toutes les matrices de poids des quatre couches — représentent environ six mégaoctets. La seule table d'embeddings de mots pèse 31 254 528 octets, car elle contient un vecteur de 256 nombres pour chacune des 30 522 entrées du vocabulaire.
Livrez-la entière et chaque visiteur télécharge 31 Mo pour observer une phrase de neuf mots, dont il utilisera neuf lignes.
L'idée décisive : la table d'embeddings n'est pas un fichier de modèle, c'est un tableau à accès aléatoire. Traitez-la comme tel et une phrase coûte des kilo-octets au lieu de mégaoctets.
HTTP a la réponse depuis 1999. Un client peut demander un intervalle d'octets plutôt qu'un fichier entier, et un serveur conforme répond 206 Partial Content avec ces octets seulement.
L'export est écrit précisément pour rendre cela possible : la table d'embeddings est un fichier binaire plat, en ordre ligne par ligne, où la ligne i commence exactement à l'octet i × 1024. Dès que le tokenizer a produit les identifiants d'entrée, chacun est un décalage en octets, et le runtime récupère exactement les lignes utilisées par la phrase.
La table d'embeddings pèse 31 254 528 octets. La scène ne récupère que les lignes dont ses tokens ont besoin, via HTTP Range : le coût suit votre phrase, pas le modèle.
Une phrase de douze mots fait environ seize tokens. Seize kilo-octets. Les 99,95 % restants de la table ne quittent jamais le serveur.
Le chargement à froid — la part que chaque visiteur paie une fois — est de 6,32 Mio : les blocs quantifiés, le manifeste et le vocabulaire. Le vocabulaire doit arriver entier, car on ne peut savoir quelles lignes seront nécessaires qu'après avoir tokenisé, et on ne peut pas tokeniser sans lui.
Cet ordre a une conséquence visible que je préfère à son alternative. La limite d'entrée est de vingt-quatre tokens, et le nombre de tokens produits par votre phrase est inconnaissable tant que le vocabulaire n'est pas là. L'interface signale donc honnêtement une phrase trop longue après coup, plutôt que de le deviner avant. Une limite prédite serait fausse précisément pour les entrées où cela compte : celles pleines de mots inhabituels qui se fragmentent.
Toute promesse « exécution locale, confidentialité par conception » sur internet porte sur du code que vous ne voyez pas. Ici c'est une propriété du déploiement, vérifiable depuis le panneau réseau de votre propre navigateur.
La Content-Security-Policy est la pièce porteuse :
default-src 'self' connect-src 'self' worker-src 'self' blob:
object-src 'none' frame-ancestors 'none' base-uri 'self'connect-src 'self' signifie que la page n'est pas autorisée à ouvrir une connexion vers une autre origine. Ni API d'inférence, ni point de collecte analytique, ni service de journalisation. Ni par accident, ni par une future modification — le navigateur refuse.
C'est une affirmation d'une autre nature que « nous n'envoyons pas vos données ». C'est : cette page ne peut pas envoyer vos données, et ce n'est pas moi qui l'applique.
Il n'y a délibérément pas de 'wasm-unsafe-eval' dans cette liste, ce qu'explique l'article précédent. Le test navigateur vérifie son absence plutôt que de compter sur la mémoire de quiconque.
Les requêtes Range ne sont pas gratuites.
Vingt petites requêtes distinctes, ce sont vingt allers-retours de latence, là où un gros téléchargement n'en a qu'un. Sur une connexion rapide, ces récupérations de lignes disparaissent dans le bruit ; sur un lien lent à forte latence, elles sont la partie la plus lente d'une requête. Le compromis vaut la peine ici, puisque l'alternative pèse 31 Mo, mais c'est un compromis.
Le serveur doit aussi coopérer. Un CDN qui ignore Range et renvoie 200 avec le corps entier ferait parfaitement fonctionner la scène tout en transférant discrètement la table complète à chaque requête — sans erreur, sans avertissement, juste trente mégaoctets par visiteur. Cette défaillance est invisible depuis l'intérieur de la page : d'où un script qui vérifie qu'un hôte réel répond 206 avec le nombre d'octets exact, au lieu de le supposer.
L'intéressant n'est pas les requêtes Range. C'est qu'une contrainte que je tenais pour acquise — « utiliser un vrai modèle dans un navigateur, c'est télécharger un vrai modèle » — s'est révélée être un artefact d'empaquetage.
Le transformeur doit être présent en entier : chaque bloc s'exécute sur chaque token. Le dictionnaire, non. Il est consulté de façon éparse, quelques entrées par requête, et le traiter comme un fichier plutôt que comme une table coûtait 31 Mo pour livrer 16 Ko d'information utile.
Question à poser devant tout ce qui semble trop lourd à livrer : est-ce un modèle, ou est-ce un index ?
Ensuite : le poids affiché se révèle être une part de quelque chose qu'on ne vous a jamais montré.
Plus à lire
L'int8 naïf plafonnait la parité d'attention à 2,97e-2 — trente fois pire que la barre. Où dépenser la précision : voilà toute l'ingénierie.
En médiane, 24 % de chaque ligne affichée avait déjà été supprimée avant que vous ne la voyiez. Au pire, 95 %. L'arithmétique était juste depuis le début.
Choisir la tête la plus confiante sélectionnait une tête positionnelle à chaque fois. Elle rapportait « it → was, 0,962 » — la réponse qu'elle donne pour chaque mot de chaque phrase.