Brimkern · Changelog
Inférence LLM accélérée par WebGPU, 100 % dans votre navigateur.
CLI Brimkern & Moteur Natif Dawn : inférence terminale on-device sans navigateur via les bindings WebGPU Google Dawn (démarrage 25× plus rapide mesuré sur Apple Metal : 0,93s vs 23,46s), flux style Claude Code / Gemini (injection @fichier, raccourcis git) et modèles de code dédiés (Qwen 2.5 Coder 0.5B & 1.5B).
Moteur WebGPU Natif Dawn in-process
- Remplacement de la dépendance à Chromium headless par des bindings Node N-API directs vers Google Dawn. La latence de démarrage passe de 23,46s sous Playwright Chromium à 0,93s sur Apple Metal (gain de 25×), et la consommation mémoire au repos chute de ~350 Mo à ~45 Mo. 100% des shaders compute WGSL s’exécutent à l’identique sur le GPU physique.
- Cache disque persistant pour les requêtes HTTP Range de tenseurs dans ~/.cache/brimkern/ranges/. Le premier téléchargement préchauffe les blocs locaux ; les exécutions suivantes atteignent 18/18 hits de cache en 0ms avec zéro requête réseau.
- Architecture de repli automatique à double runtime : Dawn natif s’exécute in-process par défaut, avec repli transparent sur Chromium headless pour la déquantification GGUF complète arbitraire ou les OS sans binaire natif (drapeaux --native et --chromium).
Nouveaux modèles de dev & Contexte @fichier
- Ajout de Qwen 2.5 Coder 0.5B Instruct (491 Mo GGUF Q4_K_M, marqué mobile: true dans les presets du catalogue) et Qwen 2.5 Coder 1.5B Instruct (1,12 Go) aux côtés de notre modèle ultra-léger LFM2.5 Coder 149 Mo et de RWKV-7 G1a 0.4B RNN.
- Outils de travail style Claude Code et Gemini CLI : injection de contexte fichier (@chemin/fichier:début-fin avec garde-fous secrets), raccourcis git (/diff, propositions /commit conventionnelles, revue /review <fichier>), copie presse-papier /copy, et métriques /stats à 0,00 $.
SDK 0.3.0 : le widget fait tourner les .brik RWKV-7 — la garde qui disait « LFM2 uniquement » était la dernière chose qui manquait, tout le moteur RWKV était déjà dans le bundle. Construit pour répondre à une question produit par la mesure ; la réponse a fermé une piste et ouvert un arbitrage précis.
RWKV-7 dans le widget
- model accepte désormais une URL de .brik RWKV-7 à côté de LFM2. Le dispatch est le miroir de celui de l’app : le moteur récurrent (kernels WGSL, drivers, chemin du tokenizer World) était déjà livré dans sdk.js — seuls la classe pure et une garde séparaient les intégrateurs des modèles sous licence Apache. Les prompts passent au format texte brut User:/Assistant: de RWKV, et la génération s’arrête aussi sur un marqueur de tour TEXTUEL : le vocabulaire World n’a aucun token spécial de tour, sans cette coupe un petit modèle écrit joyeusement les deux côtés de la conversation. Le bundle grossit de 15 Ko.
- Rien n’a bougé pour le modèle par défaut : RAG 6/6, dialogue 33/33 sur trois tours (2 rattrapages du filet seulement — le modèle en obtient 31 seul), surface d’API 50/50.
Deux mesures qui tranchent une question produit
- La question du widget léger (« le modèle par défaut peut-il peser ~100 Mo au lieu de 149 ? ») était bloquée sur un choix de modèle depuis mi-août. Le dispatch l’a rendue mesurable. RWKV-7 G1 0.1B (128 Mo, Apache) : 6/24 sur les cas RAG du widget — il sert son refus canonique alors que la bonne fiche est sélectionnée sous ses yeux, et quand il lit, il recopie la fiche entière, montant interdit compris. Lire des fiches est simplement au-dessus de ce qu’un 0.1B sait faire ; cette piste est fermée quel que soit le format du fichier.
- RWKV-7 G1a 0.4B (304 Mo, Apache) : 10/12 — refus, salutations et désambiguïsation de deux nombres d’une même fiche tiennent tous ; les deux échecs sont le même cas unique, lire une ligne dans un tableau de tailles (il répond 26,0/26,5 cm au lieu de 27,0). L’arbitrage de licence est désormais un chiffre, pas une impression : l’option Apache coûte deux fois le téléchargement et perd la lecture de tableau face au défaut de 149 Mo (12/12, licence LFM 1.0).
SDK 0.2.1 : la classification et la génération one-shot rejoignent le chemin GPU résident — ×20 mesuré sur la démo /local-ai, et l’enrichissement de prompt vidéo en profite au passage. Plus un verrou sur l’état GPU partagé : deux widgets d’une même page ne peuvent plus se corrompre mutuellement la réponse en silence.
La classe pure rejoint le chemin résident
- Le chat du widget tourne sur le chemin résident depuis la 0.1.x — prefill par tranches sur le GPU, un readback de ~512 octets par token. Mais classify() et les appelants directs de generate() (la démo /local-ai, l’enrichissement de prompt vidéo) payaient encore le forward JS historique : une dizaine de soumissions GPU et un readback du vocabulaire entier PAR TOKEN DE PROMPT. La classification est à 100 % de la lecture de prompt : c’était donc le pire cas du moteur entier.
- Mesuré avec un banc neuf (bras alternés sur la même page, le chemin JS comme témoin via un kill-switch, et la SORTIE vérifiée à chaque tir — une accélération qui change la réponse n’est pas une accélération) : classification 4,2 s → moins de 0,2 s (×20), extraction 2,8 s → moins de 0,2 s (×14). « Moins de 0,2 s » est le plancher de résolution du harnais lui-même : les deux ratios sont des minorants. Les réponses sont identiques dans les deux bras.
- Rien ne change pour les intégrateurs : même API, mêmes réponses, et le chemin JS reste en repli automatique partout où le résident n’est pas disponible. Les bancs existants : RAG 24/24 (deux tours, deux langues), dialogue 33/33, surface d’API 50/50. Les outils ont mesuré 14/15 avec un échec qui change de tour en tour — et le bundle 0.2.0 figé, étranger à ce changement, échoue le même cas de la même façon : variance d’échantillonnage d’un 230 M qui ignore son fait injecté un tirage sur quelques-uns, pas une régression du portage.
Un verrou sur l’état GPU partagé
- L’état GPU récurrent (K/V d’attention, état conv) est un slot unique par moteur — et le moteur est un singleton par URL de modèle, partagé par tous les widgets et sessions de la page. Deux générations simultanées se volaient silencieusement ce slot et produisaient toutes deux une sortie invalide. Les entrées résidentes sont désormais sérialisées par modèle : le second appelant attend son tour au lieu de corrompre le premier. Rien ne l’empêchait avant ; ça n’était simplement pas encore arrivé sur une page à un seul widget.
SDK 0.2.0 : le widget gagne des outils — l’arithmétique, la date du jour, vos propres fonctions — et cesse d’imposer son aspect : thème (clair, sombre, ou celui du système du visiteur), coin, taille, et chaque libellé peuvent désormais être les vôtres. Le README promettait « Tools are next » depuis la 0.1.4 ; les voici, conçus de la seule façon qui marche de façon mesurable à cette taille de modèle.
Outils : le modèle ne décide jamais, et c’est le principe
- tools: ['calc', 'date', { name, match, run }] sur embed() comme sur createSession(). La conception est celle que l’app a déjà validée en production : sous ~3B de paramètres, laisser le modèle émettre des appels d’outils produit des appels hallucinés — donc aucune décision ne lui est déléguée. La détection est déterministe (une regex ou votre prédicat), votre fonction tourne dans votre page, et le modèle reçoit le résultat comme un fait entre crochets dans le tour, exactement comme une fiche de connaissance. Un 230 M est notoirement mauvais en arithmétique ; avec 'calc' il répond à 127×9 le chiffre exact, parce que le chiffre lui est donné.
- Trois constats au banc, chacun corrigé par une leçon que ce moteur connaissait déjà. Une ligne de date dans le prompt système ne suffit pas : à « What year is it? », le modèle servait son refus appris (« my knowledge is based on data up until July 2024 ») sans lire la ligne posée deux phrases plus haut — une note entre crochets sur les questions de date l’a corrigé (« It is 2026. »). Un outil custom recevait « I don’t have access to real-time inventory data » juste à côté d’une note qui contenait précisément le stock demandé : la phrase de cadrage disait littéralement « You have no tools », devenue fausse dès qu’un outil est déclaré — elle est maintenant conditionnelle. Et la forme doit être MONTRÉE : deux exemples épinglés démontrent la recopie du fait entre crochets, fabriqués par la fonction même qui construit les vrais tours — l’exemple et le prompt ne peuvent pas diverger.
- Le contrat protège le visiteur : un outil qui lève, qui traîne (plafond de 10 secondes) ou qui ne rend rien est simplement absent du tour — le tour, lui, n’échoue jamais à cause d’un outil, la même règle que les écouteurs d’événements. Les résultats sont bornés à 600 caractères : un résultat est un fait, pas un rapport. Un événement tool annonce chaque résultat avant la génération — le jumeau de traçabilité d’onSources. Et tout est opt-in : sans champ tools, pas un octet des prompts mesurés ne change.
- Mesuré sur le vrai modèle, trois tours : 15/15 — le chiffre exact en arithmétique, l’année en cours, un stock fourni par la page qui atteint la réponse, et les deux interactions avec les fiches qui devaient tenir : le refus hors fiches reste intact quand aucun outil ne se déclenche, et un calcul gagne sur la consigne de refus quand un outil répond. Les bancs existants n’ont pas bougé : RAG 12/12 dans les deux langues, dialogue 33/33, surface d’API 50/50, plus 24 assertions unitaires sur la détection et les garde-fous, sans navigateur.
Le widget cesse d’imposer son aspect
- theme: 'light' | 'dark' | 'auto' — auto suit le prefers-color-scheme du visiteur, en direct : changez l’OS et le widget suit. Le défaut reste le clair, l’aspect exact que chaque intégrateur actuel a validé sur sa page. position l’ancre en bas à droite ou en bas à gauche ; width et height sont des nombres bornés (300–480 × 380–720), et les petits écrans plafonnent toujours au viewport. Tout se pose en propriétés personnalisées par élément — la règle apprise avec la couleur d’accent : une feuille de style partagée ne doit rien contenir de propre à un widget, sinon le second hérite en silence du premier. Deux widgets d’une même page peuvent désormais différer de thème, de coin et de couleur, et c’est vérifié dans un vrai navigateur.
- labels surcharge n’importe quelle chaîne du widget — bouton et croix, placeholder, la mention « IA locale », le préfixe d’erreur, les phrases de repli et d’aide, l’étiquette des sources, les phases de chargement. lang couvre l’anglais et le français d’origine ; labels est la porte vers toutes les autres langues, ou simplement vers votre propre ton. Les libellés sont rendus comme du texte, jamais comme du balisage, et theme prend un mot-clé, pas du CSS : rien de ce qu’un intégrateur passe ne peut injecter du style ou du balisage dans la page hôte — la règle de safeAccent, étendue à chaque nouvelle option.
- Et un trou refermé au passage : un embed() sans lang ni system tombait silencieusement en anglais — sur un site français, un visiteur francophone recevait un widget anglais. Quand la config n’offre rien à deviner, la page décide désormais (lang du html, puis langue du navigateur). Le verdict du prompt système reste souverain quand il existe : les consignes doivent suivre la langue du comportement déclaré, pas celle de la page qui l’héberge.
L’assistant embarquable a cessé de répondre « je n’ai pas cette information » à tout ce qui sort de ses fiches — mesuré de 3/11 à 33/33. Et quatre choses signalées le même jour : une jauge de stockage qui contredisait son propre chiffre, un bouton que personne ne pouvait interpréter, les modèles déjà téléchargés enterrés en bas de liste, et des pages de doc qui défilaient sur 583 px de vide.
SDK 0.1.4 : le widget sait de nouveau tenir une conversation
- Signalé avec une transcription : après une bonne réponse sourcée, chaque message suivant recevait le même mur. « are you for real ? » → I do not have that information. « are you ok ? » → I do not have that information. « hello », « PLEASE », « HELP ME », « I DIE » — tous. La cause : dès que le tri ne retenait aucun passage, la consigne injectée était un refus, et seule une liste blanche de salutations y échappait. Ce refus est juste pour « Qui a gagné la Coupe du monde 1998 ? » — c’est la promesse du produit — et absurde pour « ça va ? ». Pire, un modèle de 230 M recopie ce qu’il vient d’écrire : trois refus dans l’historique et la conversation ne s’en relevait plus.
- Les deux situations sont maintenant distinguées EN CODE, pas confiées au modèle : un message qui demande une information (au moins deux termes de contenu et une forme interrogative) reçoit le refus honnête ; tout le reste reçoit une consigne courte, répondre brièvement et aimablement. Le tri est déterministe et a été vérifié terme par terme sur la transcription signalée — à cette taille de modèle, une consigne qui dit « refuse si c’est une question de fait, sinon bavarde » fait deux règles, et deux règles se brouillent.
- Deux choses de plus étaient nécessaires, et les deux viennent d’une leçon que ce moteur connaissait déjà. Un exemple épinglé MONTRE le comportement qui manquait — sans lui le modèle n’avait qu’UNE façon de répondre sans fiche sous les yeux, le refus, et il la recopiait. Et un filet déterministe rattrape ce que la consigne n’obtient pas : sur un message que nous avons nous-mêmes classé conversationnel, un refus de renseigner est certainement faux, donc il n’atteint pas le visiteur. Même mécanisme que la re-salutation réflexe retirée dans le moteur : à 230 M, ce qu’une consigne n’obtient pas, un traitement déterministe l’obtient.
- Mesuré, avec un banc qui rejoue la transcription signalée dans l’ordre sur une seule conversation — l’effet de disque rayé ne se voit qu’en séquence. 3/11 avant. 25/33 sur trois tours avec la seule consigne (le disque était cassé : « hello », « ça va ? », « ALLO ? » à 3/3, et seules les interjections d’un mot et le tour suivant un refus légitime passaient encore à travers). 33/33 avec le filet, dont 4 rattrapages seulement : le modèle en obtient 29 tout seul. Le banc compte les rattrapages à part volontairement — un harnais qui cache son propre filet ne mesure rien. Le banc RAG reste à 12/12 dans les deux langues : l’exemple ajouté n’a rien dégradé.
- Un garde-fou sur le build, après qu’il a mordu : un simple `npm run build:sdk` a réécrit en silence `public/sdk-0.1.3.js`, une heure après la publication de cette version sur npm. Ce fichier doit rester identique à l’octet au paquet npm du même numéro — c’est ce qu’un intégrateur épingle pour que son widget ne change pas sous ses pieds. Le build refuse désormais d’écraser un fichier figé dont le contenu différerait, et dit de bumper la version. C’était déjà arrivé une fois, sur 0.1.1, qu’il avait fallu restaurer depuis le tarball publié.
Quatre signalements, quatre correctifs
- « J’ai tout supprimé et la barre reste pleine, 2,3 Go / 2 Go. » La jauge de stockage était tirée du total que le navigateur compte pour l’origine — son propre cache HTTP compris, qu’aucune page ne peut purger — donc elle contredisait le chiffre écrit juste au-dessus, « Données Brimkern », qui tombait bien à zéro. La barre a maintenant deux segments : notre part en accent, et en gris ce que le navigateur compte en plus, chiffres à l’appui dès que l’écart dépasse 4 Mo. Tout supprimer fait disparaître le segment accent : ce qui remplit la barre est visiblement étiqueté comme n’étant pas à nous.
- « Je ne comprends pas le bouton Nettoyer maintenant. » Il ne voulait rien dire tout seul : il applique la règle du menu déroulant d’à côté, tout de suite au lieu d’attendre. C’est écrit, avec le délai choisi dans la phrase. Et un clic qui ne trouvait rien à supprimer retombait sur le bilan précédent — le panneau réaffichait donc un ancien nettoyage comme s’il venait d’avoir lieu, ou n’affichait rien du tout. Dans les deux cas le bouton semblait cassé. Il dit maintenant : rien à nettoyer.
- Sélection de modèles : ce qui est déjà téléchargé passe devant. La grille s’affichait dans l’ordre du catalogue, donc un modèle déjà sur le disque — le seul qui démarre sans réseau et sans attente — pouvait se trouver en septième position sous six modèles à télécharger. Le tri est stable : à l’intérieur de chaque groupe l’ordre du catalogue est conservé, et rien ne bouge tant qu’aucun modèle n’est en cache.
- La doc faisait défiler 583 px après la fin de son contenu. Surligner le chapitre courant demande qu’un titre de section franchisse une ligne de lecture au tiers haut de l’écran, et les dernières sections n’y arrivent jamais : il n’y a plus de défilement à leur donner. Le correctif était une cale réservant exactement l’espace manquant ; elle marchait, et elle coûtait ce vide. La fin de document est maintenant traitée dans la règle, et un clic de sommaire épingle son chapitre le temps que le défilement retombe : sur une page qui ne défile que de 181 px, cliquer un chapitre en surlignait un autre. Quatre assertions par page le gardent désormais.
embed() rend désormais une poignée : un widget se démonte, se pilote et s’observe — et une réponse dit laquelle de vos fiches l’a produite. La démo du SDK est bilingue par ailleurs, avec l’anglais en défaut, et les libellés du widget suivent la page au lieu de parler français en toutes circonstances. Ailleurs, un GGUF que ce moteur ne sait pas lire le dit désormais au chargement.
SDK 0.1.3 : un widget qu’on pilote, qu’on démonte et qu’on observe
- embed() ne rendait rien : un widget monté était monté pour toujours. Dans une application à navigation côté client, il survivait à chaque changement de route, et un second embed() empilait un second bouton sur la page — le nettoyage d’un effet React n’avait rien à appeler. Il rend maintenant une poignée : open/close/toggle, ask() pour envoyer une question comme si le visiteur l’avait tapée, destroy() pour retirer le DOM et annuler une génération en cours. Le moteur reste chargé, parce que les poids sont partagés par la page : démonter un widget ne fait jamais retélécharger le modèle au suivant.
- Le widget était une boîte noire : un intégrateur ne pouvait ni journaliser une conversation, ni mesurer l’engagement, ni même apprendre qu’un navigateur de visiteur n’avait pas WebGPU — cet échec mourait dans une bulle de chat, alors que c’est la statistique la plus utile du widget. La poignée comme les sessions exposent maintenant on(event, callback), qui rend sa propre fonction de désabonnement : ready, progress (clé de phase stable et octets), open, close, message (la question dès l’envoi, la réponse quand elle est complète), error — qui porte le code « no-webgpu » pour ce seul échec qu’un visiteur peut déclencher sans rien faire de mal. Un écouteur qui lève est intercepté et journalisé : le code d’analytics de l’hôte n’est pas le nôtre, il ne peut pas interrompre une génération.
- Une session précharge désormais avant son premier tour, au lieu de charger implicitement à l’intérieur. Ce premier ask() téléchargeait 149 Mo sans qu’aucun callback ne puisse le dire — progress n’avait nulle part d’où venir. Même résultat, même chargement idempotent partagé par la page ; il est simplement observable.
- Une réponse peut désormais nommer ses sources. Les passages retenus dans vos fiches étaient sélectionnés, injectés dans le prompt, puis jetés : quand l’assistant répondait de travers, rien ne disait si la mauvaise fiche avait été choisie ou la bonne mal lue. session.ask() prend un callback onSources, les sessions exposent lastSources, l’événement message porte la même chose, et showSources: true les affiche sous chaque réponse dans le widget. Sur un modèle de 230 M, une réponse que le visiteur peut vérifier vaut mieux qu’une réponse simplement affirmée.
- Une conversation se reprend, et une base de connaissance se remplace sans la perdre. history est accepté à la création et setHistory() la remplace ensuite : ranger widget.history et le redonner rend son fil au visiteur après un rechargement — le type Msg est exporté pour exactement cela. L’accueil est alors ignoré au lieu de s’empiler par-dessus : un tour d’assistant de plus dans une fenêtre courte pousse le vrai contexte dehors, et cela dégrade de façon mesurable le choix des fiches. Les tours invalides sont jetés en silence, parce que ce qui arrive là vient d’un stockage persistant, et faire échouer le montage d’un widget pour un enregistrement du mois dernier qui a un champ de trop serait une punition sans rapport avec la faute. setKnowledge() redécoupe vos documents sur place : mettre un catalogue à jour imposait de détruire la session et d’en créer une autre — donc de jeter la conversation pour changer un prix. Les deux lèvent pendant une génération : la boucle en cours se rétracte sur la dernière entrée de l’historique.
- Deux défauts trouvés en mesurant ce qui précède. Une page portant deux widgets les affichait de la même couleur : la feuille de style est partagée et injectée une seule fois, donc l’accent qu’on y interpolait était celui du premier widget monté — il voyage maintenant sur l’élément, et plus aucune valeur d’intégrateur n’entre dans du texte CSS. Et le modèle recopiait parfois notre propre marqueur de fin dans sa réponse (« … sous 5 jours ouvrés. --- END OF NOTES ») : ce marqueur délimite le bloc de connaissance dans le prompt, il n’a rien à faire dans une bulle de chat.
- Notre propre page de démo contournait l’absence d’API, ce qui est le meilleur argument qu’il en fallait une. Pour poser une question depuis la page, ses puces de suggestion devaient deviner les classes internes du widget, cliquer sur le bouton, attendre 80 ms au hasard puis écrire dans le champ — et encore, elles ne faisaient que le REMPLIR, le visiteur devait valider lui-même. C’est maintenant widget.ask(text) : une ligne, et la question part vraiment. La démo active aussi showSources, pour que chaque réponse montre les fiches dont elle vient.
- Deux bancs étayent tout ceci. Le banc RAG gagne un jeu de cas anglais — la démo était bilingue mais seule sa moitié française était mesurée, et les deux langues ne se comportent pas pareil — et il lit maintenant les sources de chaque réponse : un échec dit lequel des deux maillons a lâché, la mauvaise fiche sélectionnée ou la bonne mal lue. 12/12 dans les deux langues. Un second harnais vérifie la surface publique elle-même dans un vrai navigateur sans télécharger un octet de poids : 33 vérifications, dont le fait que deux widgets gardent chacun leur couleur et qu’un widget détruit quitte réellement la page.
- Un comportement change : une session programmatique avec des documents de connaissance échantillonne désormais à 0,25 comme le widget, et non plus à 0,55. Le widget avait été mesuré — à 0,55, la lecture d’une ligne de tableau partait une fois sur trois sur la mauvaise colonne — et la session avait conservé la valeur haute en silence. Même prompt, même comportement des deux côtés ; une température déclarée explicitement continue de primer.
Chargement d’un GGUF : un type non supporté se dit, au lieu d’échouer plus loin
- Un type GGML que le moteur ne sait pas déquantifier retombait sur « 1 élément, 4 octets », ce qui rendait FAUSSES toutes les tailles de tenseurs du fichier : le modèle se chargeait, puis mourait beaucoup plus loin sur une lecture hors bornes sans rapport apparent avec la cause. Coller un des fichiers quantifiés IQ qui existent en vrai — le dépôt unsloth de LFM2.5 publie IQ4_XS, IQ4_NL, UD-IQ2_M et UD-IQ3_XXS à côté des K-quants — est exactement la façon de tomber dessus. Le parseur nomme désormais l’énumération GGML complète et lève à la seule place où l’information est encore lisible : « Type GGML non supporté : IQ4_XS », avec la liste de ceux qu’il lit.
- Tout ceci sort de la question « peut-on descendre sous 149 Mo pour le modèle du widget ? ». La réponse est mesurée et c’est non — retirer un bit par poids fait perdre la tâche, pas un peu de qualité — et l’outillage qui l’a établie est conservé : un déquantificateur des K-quants côté Node (testé), un hook de dtype par tenseur dans le convertisseur .brik, et des tiers de build capables de rejouer tenseur par tenseur l’allocation de bits d’un GGUF calibré par imatrix. Quel que soit le prochain modèle candidat, il se mesurera avec les mêmes instruments plutôt qu’avec une intuition.
La même version : le widget parle la langue de votre page
- Les libellés du widget étaient écrits en français dans le code : un site anglais qui posait la balise script recevait un placeholder français (« Écris un message… »), une mention de confidentialité française et des bulles de chargement françaises sous un contenu anglais. Ils suivent maintenant la même option lang que les consignes — déclarée, ou devinée depuis votre prompt système comme avant : une seule option et tout le widget s’accorde à la page.
- Les phases de chargement ne sont plus des phrases françaises mais des clés stables (init, download, tokenizer, gpu) : le widget les rend dans sa langue, et un intégrateur qui affiche le status de preload() les libelle dans la sienne. Le seul échec qu’un visiteur peut déclencher sans rien faire de mal — un navigateur sans WebGPU — porte désormais un code de cause, donc il est formulé dans la langue de la page et non dans celle du moteur.
- La démo live est bilingue : /sdk-demo sert l’anglais (la version canonique, comme partout ailleurs sur ce site) et /fr/sdk-demo le français, avec une bascule entre les deux. Les deux boutiques portent les mêmes règles et les mêmes chiffres en deux langues ; le jeu français est inchangé au caractère près, parce que c’est celui que le banc RAG note 6/6.
La génération d’image est 3,6× plus rapide et sort de bêta. Puis un audit du même jour a trouvé ce que la vitesse cachait : un défaut desktop qui pointait un modèle que ce moteur ne sait pas exécuter, huit combinaisons format×résolution qui dégradaient l’image en silence, et des réductions GPU carrément fausses sur les puces Intel et Mali. Tout est corrigé — et les modèles que nous publions sont désormais vérifiés par empreinte avant d’être chargés.
Génération d’image : ×3,6, et hors bêta
- Les convolutions 3×3 calculent maintenant 8 canaux de sortie par workgroup, en partageant un seul chargement de poids au lieu de le relire par canal : ×3,05 sur une génération complète (17,0 s → 5,6 s). Les convolutions 1×1 ont reçu le même traitement deux heures plus tard : 4,80 s, ×3,58 contre le point de départ.
- La génération d’image sort de bêta : 512 px par défaut sur un GPU capable (le modèle est entraîné en 512 — en dessous il ne compose plus, il recadre), annulation, enregistrement, et une barre de progression qui montre du travail réel avec un temps restant.
- Le format et la résolution sont désormais deux choix distincts : 7 formats (du 1:1 au 9:16) × 5 résolutions. Chaque libellé annonce la taille que vous obtiendrez vraiment — sur un modèle natif 512, demander du HD rend en 512 puis agrandit ×2 sur le GPU (bicubique Catmull-Rom et rehaussement d’arêtes), et l’option le dit au lieu de promettre un rendu natif.
- Le décodeur VAE complet de Stable Diffusion a été écrit, quantifié en int8 et mesuré : 3,4× plus lent que TAESD (25,5 s contre 7,4 s) et limité par la mémoire, pas par le calcul, en 512². Écarté comme défaut — et c’est lui qui a désigné les convolutions comme le vrai goulot.
Ce que l’audit a trouvé
- Le défaut desktop avait basculé sur un modèle SDXL. Deux problèmes : ses poids répondaient 404, et ce moteur n’a aucun chemin SDXL (un seul encodeur de texte, pas de conditionnement additionnel) — le repli partait donc chercher 10,27 Go de fp32 pour une topologie qu’il ne sait pas lire. Retour à SD-Turbo, et la garde de reprise de conversation résout maintenant ses URLs par la même fonction que le chargeur : les deux avaient divergé, et la garde laissait passer exactement le gros téléchargement qu’elle devait empêcher.
- Huit des 42 combinaisons format×résolution produisaient des latents dont les côtés n’étaient pas multiples de 8. Le UNet divise trois fois par deux puis redouble : sur un côté impair, la remontée retombe un pixel à côté de son skip, et la concaténation lit au-delà du tampon. WebGPU borne les lectures hors plage, donc aucune erreur et aucun log — juste une image silencieusement dégradée. Deux des huit étaient atteignables depuis l’interface. Corrigé, avec un test qui rejoue le forward dimension par dimension sur les 42 ; il a aussi attrapé le 16:9 « équilibré », qui rendait un 3:2 exact.
- Les nouvelles normalisations accélérées par subgroups supposaient un sous-groupe de 32 voies. C’est vrai sur Apple, NVIDIA et AMD ; sur les GPU intégrés Intel (8 ou 16) et Mali (4 ou 16), la plupart des sommes partielles étaient jetées en silence, donc toutes les normalisations du modèle sortaient fausses. Elles sont maintenant dimensionnées pour le plus petit sous-groupe possible et, comme tous les autres kernels ici, validées contre l’implémentation de référence au chargement du moteur, avec repli automatique.
- Le mobile était repassé à 512 px — précisément le pic mémoire qui fait reprendre le GPU par le système en pleine génération, la raison même de ce plafond. Le plafond est rétabli, et le sélecteur ne propose que ce que la machine encaisse.
Les modèles que vous chargez sont vérifiés
- Chaque modèle que Brimkern choisit lui-même porte désormais une empreinte SHA-256 de son manifeste, vérifiée avant que le premier tenseur soit demandé ; un écart refuse le chargement plutôt que de faire tourner un modèle inconnu sous notre nom. Le manifeste décrit tout le paquet — architecture, tokenizer (sur un modèle de langage il embarque le vocabulaire, 5 à 12 Mo, donc c’est couvert aussi), table des tenseurs, tailles de shards. Il ne couvre pas les octets des tenseurs eux-mêmes, et nous préférons le dire que laisser croire l’inverse.
- Les poids de tiers (les fichiers SD-Turbo, le décodeur TAESD, le modèle de vision) sont épinglés à un commit précis au lieu d’une branche. Une branche est un pointeur mouvant : qui contrôle ce dépôt peut changer le fichier sous nos visiteurs. Nous ne mettons jamais ces fichiers à jour, donc le pointeur mouvant n’apportait rien et coûtait cette exposition.
- Un manifeste arrivé par le réseau est désormais validé avant que ses nombres dimensionnent le moindre tampon GPU : bornes plausibles sur chaque champ, et surtout chaque tenseur doit tenir dans son propre shard, donc aucune lecture planifiée ne peut sortir de la zone de données annoncée. Un identifiant de tokenizer est restreint à la forme « auteur/dépôt » — il devient une requête réseau, et un identifiant forgé pouvait charger le tokenizer d’un inconnu.
Vision : 4,6× plus rapide, et lit aussi bien
- L’image envoyée à l’encodeur visuel avait été agrandie de 448 à 896 px « pour le texte et les interfaces », sans mesure. Le coût est quadratique. Mesuré sur une mire de quatre lignes de 72 à 13 px : 448 px lit les quatre lignes en 11,4 s (256 tokens visuels) ; 896 px lit les mêmes quatre en 52,8 s (729 tokens). ×4,6 sur la réponse pour zéro ligne de plus — 896 rend la casse plus fidèlement, ce qui ne vaut pas 40 secondes. Retour à 448, et le banc est dans le dépôt pour que le prochain changement de ce nombre arrive avec des chiffres.
SDK 0.1.2 : le widget répond depuis vos fiches
- La sélection dans la base de connaissances est 9,4× plus rapide (1,58 → 0,17 ms par question sur 120 passages) : l’analyse des mots d’un passage était refaite pour chaque passage et chaque terme de la question. Elle est calculée une fois par passage. La réécriture a été prouvée équivalente, pas supposée : 48 000 comparaisons de scores contre l’implémentation précédente, zéro écart.
- Le widget recevait la bonne fiche et répondait à côté : interrogé « je fais du 42, quelle taille en cm ? » avec « EU 42 : 27.0 cm » sous les yeux, il répondait « le 42 est une taille en cm », et il répondait en anglais à deux questions françaises. Un banc navigateur note maintenant six cas sur la boutique de démonstration (lecture d’une ligne de tableau, deux nombres dans une même fiche, refus hors sujet, bavardage) : on est passé de 2/6 à 6/6, stable sur dix sessions séparées. Quatre choses étaient fausses, mesurées une par une. La consigne qui encadre les fiches était toujours en anglais, et le modèle suivait sa langue plutôt que celle de la question. Les tours de démonstration étaient écrits à la main et avaient dérivé des vrais — ils montraient les notes sans la ligne de consigne qui les précède, et à cette taille de modèle une forme non démontrée est une forme non suivie ; ils sont désormais fabriqués par la même fonction que les vrais tours, donc la dérive est impossible. Deux opérations n’étaient jamais démontrées : lire une ligne dans un tableau, et choisir le bon nombre quand une fiche en contient deux. Enfin la génération tournait à 0,55 de température — bon pour du bavardage, mauvais pour recopier un chiffre ; une session avec fiches tourne maintenant à 0,25.
- Une fiche qui partage un simple mot avec la question n’est plus injectée. À « la livraison est gratuite à partir de combien ? », la sélection renvoyait la fiche Livraison (score 0,695) et la fiche Retours (0,228, pour le seul mot « gratuits ») — et le modèle répondait depuis la seconde. Un passage à un tiers du score du meilleur est du bruit quel que soit le seuil absolu : la sélection ne garde plus que ce qui est comparable à son meilleur.
- La langue des consignes peut désormais être déclarée (lang: 'fr' | 'en') au lieu d’être devinée depuis le prompt système. La devinette reste en repli, avec des frontières de mots plus strictes — « aide » matchait à l’intérieur de mots anglais.
- Une version épinglée du SDK est maintenant vraiment figée. Le sdk-0.1.0.js du site avait divergé du brimkern@0.1.0 de npm — même nom, octets différents — et le test de fraîcheur exigeait en fait de reconstruire les fichiers épinglés, soit l’inverse de ce que l’épinglage promet. À partir de 0.1.1, le fichier auto-hébergé et le paquet npm sont identiques à l’octet — vérifié contre le tarball publié à chaque incrément.
- Aussi : glisser un .gguf ou un .brik sur le navigateur de modèles fonctionne à nouveau, et un modèle se décharge à nouveau depuis sa carte — image, vidéo et vision comprises, là où se trouve précisément un gigaoctet de VRAM. Les vignettes gardent leur format au lieu d’être écrasées en carré. Une image jointe est stockée une fois au lieu de trois (environ 750 ko ramenés à 40 ko chacune). Ouvrir l’application avec la sidebar fermée faisait jeter à React la page prérendue pour tout refaire côté client ; c’est corrigé. Et le menu de la documentation surligne la bonne section sur les pages courtes.
Le plafond de calcul de la machine est enfin mesuré, et il a montré que nos multiplications matricielles étaient bridées par leur propre boucle interne, pas par le matériel. Réécrites, elles lisent votre prompt jusqu’à 1,5× plus vite au niveau kernel sur les modèles de classe 7B.
Lecture du prompt : les multiplications rattrapent leur retard
- Un nouveau banc mesure ce que ce GPU sait vraiment calculer : 2 825 GFLOP/s. Nos multiplications matricielles de lecture de prompt tournaient à 973, et une maquette de leur boucle interne plafonnait exactement à ce chiffre : le goulot était la structure du kernel (trop de lectures de mémoire partagée par multiplication), pas la machine. Le banc est dans le dépôt (scripts/e2e/flops.mjs).
- Les kernels int8 et int4 sont réécrits avec des blocs de registres plus larges (chaque thread calcule désormais 4×8 sorties, nourries par des lectures vectorisées). Mesuré kernel isolé sur les formes exactes d’un 7B : ×1,4 à ×1,5, ce qui monte le plafond théorique de lecture de prompt de ce modèle de 74 à 113 tok/s. Comme toujours : validé contre la référence CPU à chaque chargement, avec un kill-switch (?qshared2=0) et un repli automatique.
- Mesuré de bout en bout sur un petit modèle (Qwen3 0.6B, prompt d’environ 480 tokens) : la lecture passe de 506 à 556 tok/s (×1,10). Sur les petits modèles les multiplications ne pèsent plus qu’un quart de la phase de prompt depuis le correctif d’attention du 16 août, donc les plus gros gains vont aux plus gros modèles, où elles dominent.
- Confirmé le soir même sur un modèle intermédiaire (Qwen3 4B, dans l’application complète) : les multiplications de la phase de prompt passent de 3,3 à 2,4 ms par tir. ×1,48 à formes égales, exactement où le banc kernel le prédisait. À cette taille elles coûtent 8× l’attention pendant la lecture du prompt : c’était précisément le bon kernel à réécrire.
Les images se génèrent trois fois plus vite
- Les convolutions, qui portent deux tiers d’une génération, calculaient un canal de sortie par groupe de threads GPU. Le morceau d’image en entrée était donc relu depuis la mémoire une fois par canal de sortie, et chaque thread faisait neuf multiplications pour dix-huit lectures. En calculant huit canaux d’un coup depuis le même morceau, cela devient soixante-douze multiplications pour les mêmes dix-huit lectures.
- Le même traitement a ensuite été appliqué aux convolutions 1×1 des raccourcis, que le premier correctif avait propulsées à la deuxième place du budget. Mesuré de bout en bout en 512px : une génération passe de 17,2 à 4,8 secondes, ×3,58. Générer en pleine résolution native coûte désormais ce que coûtait la demi-résolution. La génération vidéo partage le même réseau et en profite aussi. La structure a été choisie par la mesure avant d’écrire une ligne du kernel : un canal par groupe atteignait 300 GFLOP/s, quatre canaux ×2,3, huit ×2,8, et au-delà le rendement s’efface.
La génération d’image passe à l’âge adulte
- Les images sont désormais générées en 512px par défaut sur un GPU capable. Ce n’est pas un réglage de confort : le modèle est entraîné en 512, et en dessous il ne compose plus. Sur le même prompt et la même seed, le 256px rendait un fragment de visage recadré là où le 512px rendait le portrait entier. Les téléphones et les petits GPU restent en 256px, où c’est la mémoire qui décide et non le goût.
- Une génération en cours peut être annulée. Le bouton stop n’interrompait que le modèle de langage : il fallait attendre la fin d’une image ou d’un clip. Il s’arrête maintenant au bloc suivant, et libère la mémoire GPU qu’il occupait.
- Les images et les clips générés peuvent être enregistrés. Un clip, en particulier, ne vivait que le temps de la session : des minutes de calcul disparaissaient à la fermeture de l’onglet.
L’attente d’une génération, rendue lisible
- Une génération affiche désormais un chronomètre, une barre qui suit le calcul réel et une estimation du temps restant, au lieu d’une ligne de texte qui changeait toutes les quelques secondes. La barre vient du pipeline lui-même : elle avance au vrai rythme plutôt que de faire semblant.
- Les clips ne s’affichent plus en 0:00 : les navigateurs enregistrent le WebM comme un flux live dont la longueur n’arrive jamais dans l’en-tête du fichier, et le lecteur la mesure maintenant au chargement. La longueur du clip se choisit aussi, de 8 à 32 frames, avec le coût de calcul annoncé en face de chaque option.
- La page de labo vidéo disparaît : la génération vidéo vit dans le chat, à côté des autres modalités, et le labo était une seconde porte vers la même pièce.
Génération d’image et de vidéo, 1,67× plus rapide
- Un nouveau banc profile une génération entière kernel par kernel, et il a trouvé le coupable immédiatement : les convolutions mangeaient 73,8 % du GPU, et l’une d’elles à elle seule (la convolution 3×3 sur poids quantifiés) en prenait 70 %, à 35 ms l’appel. Le chemin en pleine précision avait une version tuilée rapide de cette convolution ; le chemin quantifié, celui que l’application exécute vraiment, n’en avait jamais eu.
- Écrite, elle lit chaque pixel d’entrée une fois par groupe de travail au lieu de neuf, et déballe chaque poids une fois au lieu de 256. Mesuré sur une image 256px : la génération entière passe de 5,0 à 3,0 secondes (×1,67), et cette convolution de 35 à 19 ms. La génération vidéo partage le même réseau, elle en profite donc aussi. Comme toujours : vérifiée contre la référence CPU à chaque chargement, avec un kill-switch (?convtq=0) et un repli automatique.
- Un clip généré affichait une durée de 0:00 dans le lecteur : les navigateurs enregistrent le WebM comme un flux live dont la longueur n’est jamais inscrite dans l’en-tête du fichier. Le lecteur la mesure désormais au chargement, la timeline et la barre de lecture fonctionnent.
La génération vidéo entre dans le chat (bêta)
- Le labo vidéo devient un vrai mode : choisissez « Générer dans le chat » sur la carte vidéo (bureau), décrivez une scène, et un court clip en boucle est généré sur votre GPU. Avec la progression étape par étape, car un clip prend des minutes, pas des secondes. Votre prompt d’une ligne est d’abord développé par un petit modèle de langage local en vraie direction visuelle (les prompts courts font des clips statiques).
- Rouvrir une conversation vidéo montre la première frame de chaque clip plutôt que de le perdre en silence : un clip généré ne vit que la session (contrairement aux images, en régénérer un coûte des minutes, donc pas de « cliquer pour révéler »).
Attendre, et lire, dans sa langue
- Charger un pipeline image ou vidéo télécharge jusqu’à 1,5 Go, et n’affichait qu’une ligne de texte pendant ce temps. Il a désormais la même barre de progression et la même estimation du temps restant que les modèles de langage : un téléchargement lent ne se confond plus avec un téléchargement bloqué.
- Les pipelines image, vidéo et vision écrivaient leurs étapes de chargement en français uniquement : une page anglaise pouvait donc afficher « Téléchargement du module motion… ». Chaque étape suit maintenant la langue de la page.
- Le changelog et la comparaison WebLLM rejoignent la coquille de la documentation, avec le même menu latéral, et le changelog gagne une ancre par version : on peut désormais pointer un lien vers une version précise.
Le SDK arrive sur npm, et la doc gagne une colonne vertébrale
- Le SDK embarquable est désormais publié sur npm sous le nom brimkern. Types TypeScript inclus, importable côté serveur sans risque (Next.js, Remix, Astro), même API que la balise script : embed, createSession, generate, preload, status.
- Une page de référence API complète l’accompagne sur /docs/sdk : chaque option de chaque appel, documentée depuis les définitions de types publiées.
- La documentation a maintenant un menu latéral collant : les pages de la doc, plus les sections de la page courante surlignées au défilement. Sur écran étroit il se replie en rangée de pastilles.
Les réponses arrivent environ 40 % plus vite : l’étape de normalisation de chaque couche tournait sur un seul thread du GPU. Les modèles à raisonnement ne restent plus bloqués sur « Réflexion… », le stockage cesse de garder des morceaux qui ne seront jamais relus, et une comparaison mesurée avec WebLLM est en ligne.
Des réponses plus rapides
- Chaque couche normalise ses valeurs deux fois par token, et cette étape était écrite une ligne par thread : correct pour lire votre question (des centaines de lignes d’un coup), gâché pour écrire la réponse (une seule ligne, donc 63 threads sur 64 inutilisés). Réécrite pour répartir la ligne sur tout le groupe : le décodage passe de 36,0 à 49,5 tok/s (×1,38) sur un Qwen3 0.6B, la lecture du prompt est inchangée.
- La mesure qui l’a trouvé : un profileur GPU par passe (?gpuprofile=1) ajouté le même jour, qui montrait la normalisation à 51,9 % du temps de décodage. Deux fois le coût des multiplications matricielles qu’elle alimente.
- Sur un 7B le même correctif vaut ×1,27 (8,1 → 10,2 tok/s) : plus le modèle est gros, plus les multiplications matricielles dominent, donc moins la normalisation pèse. Ce 7B est désormais dans le catalogue : c’est le modèle sur lequel nos chiffres publiés sont mesurés, vous pouvez le lancer vous-même.
Modèles à raisonnement : plus de cul-de-sac
- Quand un modèle s’arrêtait au milieu de sa réflexion, la bulle restait sur « Réflexion… » pour toujours : sans réponse, sans explication, sans issue. Cet état s’affiche désormais en bloc repliable « Raisonnement (interrompu) », la réponse est marquée comme coupée, et un bouton Continuer la reprend.
- Le raisonnement des tours passés n’est plus renvoyé au modèle (les gabarits officiels Qwen3/R1 le retirent aussi) : le prompt du 2e tour passe de ~240 à 68 tokens, ce qui laisse plus de place à la conversation elle-même.
Un stockage qui ne gonfle plus pour rien
- Un modèle pouvait occuper 239 Mo de cache pour un fichier de 149 Mo : des morceaux laissés par un ancien plan de téléchargement, jamais relus mais toujours comptés dans le quota du navigateur. Celui-là même qui décide si un modèle peut être gardé. Ils sont nettoyés automatiquement, et uniquement ceux entièrement contenus dans un plus grand : rien de ce que vous avez déjà n’est perdu.
- Les modèles dont le lien de téléchargement porte une query (une forme courante chez Hugging Face) étaient invisibles pour le panneau Stockage, pour « supprimer ce modèle » et pour le nettoyage automatique. Ils sont de nouveau reconnus.
Llama, Mistral et SmolLM3 se chargent autrement
- Ces trois familles rangent deux de leurs matrices d’attention autrement que les autres. Jusqu’ici nous réécrivions ces matrices au chargement pour coller à notre kernel ; le kernel gère désormais leur convention directement. Llama 3.2, Ministral 3 et SmolLM3 ont été revérifiés et répondent juste dans les deux cas (?ropenorm=0 rétablit l’ancien chemin).
- Conséquence : ces modèles peuvent désormais être empaquetés en .brik. Cette réécriture était impossible sur un layout quantifié : c’est ce qui faisait refuser la conversion d’un GGUF Llama en .brik.
Le site
- Une comparaison mesurée avec WebLLM sur /vs-webllm : même GPU, même modèle 7B int4, prefill et décodage côte à côte. Y compris là où WebLLM est devant.
- Accessibilité : les quatre pages secondaires portaient des défauts de contraste en thème sombre qu’aucun audit n’avait couverts (le rouge des aplats servait aussi aux petits textes). Huit pages × deux thèmes passent désormais à zéro violation.
- Sur l’accueil français, le panneau terminal restait délavé en permanence : les phrases françaises, plus longues, le repoussaient sous la ligne de flottaison où son apparition liée au défilement ne se terminait jamais. Il apparaît maintenant avec le reste du hero.
- Le schéma des couches se dessine morceau par morceau au défilement, et les chiffres mesurés se comptent en arrivant à l’écran.
- La démo du SDK demandait « une petite histoire » sous un budget de 100 tokens et s’arrêtait au milieu d’une phrase. Elle demande trois phrases, et a le budget pour.
N’importe quel GGUF mono-fichier de Hugging Face tourne désormais ici : collez auteur/modèle et c’est parti. Le décodage est 4× plus rapide sur les gros modèles, la première réponse ne coûte plus dix secondes, la famille Llama répond de nouveau juste, et le site a enfin une porte d’entrée distincte de l’application.
Une porte d’entrée, et l’app à son adresse
- L’accueil est désormais une vraie landing qui explique ce que fait le moteur ; le chat vit sur /chat. Les liens déjà publiés (?model=…) atterrissent toujours dans l’application.
- Un hub de documentation sur /docs rassemble tout : charger un modèle, liens de test instantané, format .brik et convertisseur, SDK, stockage, diagnostics.
- L’anglais devient la version canonique du site (français sur /fr), chaque langue sur son URL indexable.
N’importe quel modèle du Hub, en un collage
- Collez auteur/modèle, une URL Hugging Face, ou un lien direct .gguf / .brik : la meilleure quantification est choisie et le tokenizer est lu dans le fichier. Rien à régler.
- Les gros GGUF arrivent par plages HTTP au lieu d’un téléchargement complet : un modèle de 4,7 Go recharge depuis le cache en 15,8 s.
Vitesse
- Le décodage réutilisait un kernel taillé pour plusieurs tokens à la fois, laissant sept threads sur huit inutilisés. Un kernel dédié : ×4,2 sur les formes 7B (3,4 → 14,4 tok/s), ×2,4 sur un 0.5B.
- Le premier message d’une session payait la mise en VRAM des poids (10,9 s sur un 7B). Une préchauffe jetable déplace ce coût hors de votre première question : 1,1 s.
- Les GEMM du prefill sont tuilés et bloqués en registres dans les trois précisions : ×2–2,7 au niveau kernel, et ~1 TFLOP/s tenus sur les formes 7B.
La famille Llama répond de nouveau juste
- Llama 3.2 produisait du charabia fluide. Cause : une optimisation de chargement (une plage HTTP par couche) remplissait le cache de poids directement, en sautant la correction de lignes que ces modèles exigent sur leurs matrices Q/K. Le chat lisait donc des poids mal ordonnés. Corrigé ; Llama répond juste, et une référence CPU valide le moteur couche par couche.
Le quotidien
- Les modèles inutilisés depuis 30 jours sont nettoyés automatiquement (réglable, ou désactivable). Le panneau Stockage regroupe par modèle au lieu de lister des centaines de plages HTTP.
- La recherche web ne se déclenche plus sur du bavardage, une réponse coupée le dit et propose Continuer, et les blocs de raisonnement sont masqués par défaut.
- Accessibilité : 7 violations → 0 (contrastes, libellés de champs, points de repère, ordre des titres), en clair comme en sombre.
Brimkern passe open source (MIT), et devient embarquable : une balise <script> suffit pour poser une IA locale sur votre propre site. Le chat ultra-léger ne gèle plus, et la génération vidéo gagne un moteur résident et un vrai export WebM.
Open source : le code est public
- Tout le moteur est désormais sur GitHub sous licence MIT : kernels WGSL, format .brik, chargeurs, application. Nouvelle adresse : brimkern.romainkhanoyan.fr.
- Un README produit avec captures d’écran, et la plomberie SEO qui va avec (robots, sitemap, domaine canonique).
SDK embarquable (v0) : votre site, le GPU de vos visiteurs
- Une balise <script src="/sdk.js"> + Brimkern.embed({ system: … }) monte un widget de chat qui exécute un modèle .brik entièrement sur le GPU du visiteur : zéro serveur, zéro coût par token, privé, hors-ligne après le premier chargement. Exemple live sur /sdk-demo.html.
- Configurable avec un simple objet : modèle (URL d’un .brik hébergé), prompt système, titre, message d’accueil, couleur d’accent, budget de tokens. Le modèle ne se télécharge que quand le visiteur ouvre le widget : votre PageSpeed reste intact.
- Il réutilise le chemin rapide de l’app (décodage GPU résident) et n’interprète jamais la sortie du modèle comme du HTML : texte brut uniquement, styles cantonnés au widget.
Le chat LFM2.5 dégelé, et 2,3× plus rapide
- Basculer sur LFM2.5 en cours de conversation pouvait geler l’onglet : chaque token déclenchait ~100 allers-retours GPU, et rejouer tout l’historique les multipliait par milliers. Le calcul est désormais 100 % résident GPU : une soumission par token, une seule pour tout le prefill.
- Vérifié token-identique à l’ancien chemin avant livraison, avec repli automatique et interrupteur ?lfm2resident=0. Mesuré : 13,5 → 31 tok/s sur la même machine.
Vidéo (labo) : moteur résident, enrichissement de prompt, export WebM
- Les modules temporels (motion) tournent désormais entièrement sur le GPU en une seule soumission : 5× plus vite par module, avec repli vérifié contre la référence CPU et ?videoresident=0.
- Votre prompt court est enrichi par le modèle local LFM2.5 en vraie description cinématographique avant la génération : un meilleur mouvement, toujours 100 % on-device.
- Le résultat s’exporte en clip WebM bouclé à durée bornée (~10 s) au lieu de frames brutes.
RWKV-7 rejoint le catalogue : attention linéaire, mémoire constante
- RWKV-7 G1 0.1B se charge depuis le navigateur de modèles : une architecture 100 % récurrente où un état fixe d’environ 1 Mo remplace le cache KV. La mémoire ne grandit pas avec la conversation. 128 Mo, Apache-2.0, tokenizer World embarqué, réponses simples mais honnêtes (c’est un 0.1B).
Mobile & entretien
- Le navigateur de modèles complet est désormais accessible sur mobile avec une action « Changer de modèle » : Qwen 3 0.6B et compagnie sont à un tap, plus réservés au desktop.
- La jauge de stockage affiche désormais le vrai quota du navigateur (il dépend de l’espace disque libre) au lieu d’une estimation optimiste, le splash de première visite trompeur a disparu, et la carte « Moteur GPU » est plus compacte.
Un second moteur est né : les modèles à attention linéaire et hybrides tournent dans ton navigateur. Un modèle de 149 Mo qui discute en français, une démo live sur /local-ai, et la première vidéo jamais générée par Brimkern, entièrement sur ton GPU.
LFM2.5 230M : l’ultra-léger qui discute vraiment (mobile aussi)
- Nouveau chemin moteur pour les architectures hybrides (convolution courte + attention) : LFM2.5-230M tourne de bout en bout sur nos kernels WGSL. 149 Mo, répond en français propre, ~24 tok/s.
- Disponible dans le chat en preset, et sur mobile, où tu choisis désormais entre LFM2.5 (149 Mo, recommandé) et Qwen 0.5B (378 Mo).
- Chaque kernel validé contre un oracle CPU, token par token face à llama.cpp avant livraison.
Démo live sur /local-ai : classer, extraire & discuter
- Sentiment (12/12 sur nos bancs), extraction d’email avec garde-fou anti-hallucination (un email absent de ton texte n’est jamais inventé), et un petit chat francophone : le tout dans ton navigateur, en cache après le premier téléchargement de 149 Mo.
- Classification contrainte sous le capot : le modèle ne peut répondre que dans le jeu d’étiquettes autorisé. La technique qui rend les petits modèles fiables.
Première vidéo générée dans le navigateur (labo)
- 16 frames temporellement cohérentes (un renard qui marche dans la neige) en ~3,5 minutes, 100 % local : les modules motion AnimateDiff-Lightning greffés sur notre pipeline image existant. Zéro nouveau kernel GPU nécessaire.
- Encore en labo (banc dev) : l’UI produit, le pacing thermique et l’export WebM arrivent ensuite.
Correctifs & entretien
- Rouvrir une conversation contenant des images ne re-télécharge plus le modèle image : seuls les modèles texte se chargent automatiquement, tes images sauvegardées s’affichent telles quelles.
- Badge « En cache » renommé « Téléchargé » avec un style plein distinct : fini la confusion avec le verdict GPU « Tourne bien ».
Brimkern apprend à voir : montre-lui une photo et pose tes questions. Entièrement sur ton GPU. Affiner une image repart désormais de ses vrais pixels, et le dernier kernel différé est livré.
Vision (bêta, desktop) : image + texte → texte
- Qwen2-VL 2B tourne entièrement dans le navigateur : un encodeur visuel de 675 M de paramètres (32 couches transformer, positions rotatives 2D) lit l’image en patches, un « merger » les projette dans le modèle de langage, et le LLM répond à tes questions dessus. Les deux fichiers de poids streament depuis Hugging Face et restent en cache (~2,3 Go, GPU de bureau uniquement).
- Sous le capot : deux nouveaux kernels de position (RoPE 2D pour la tour visuelle, M-RoPE pour le modèle de langage). Le second touche le chemin chaud du chat, donc il ne se déclenche QUE pour cette architecture et s’auto-teste au chargement, avec un repli qui coupe la vision, jamais le texte.
- Dans le catalogue, la carte Qwen2-VL est désormais chargeable (desktop). Joins une image avec le bouton 📎 et pose tes questions : le multi-tours fonctionne, tout reste local.
Deux nouveaux cerveaux au catalogue : Qwen 3, et Llama est de retour
- Qwen 3 (4B et 0.6B) : la génération suivante, nettement plus forte que Qwen 2.5 à taille égale, avec le raisonnement pas-à-pas natif. Le sélecteur de budget de réflexion s’y applique. Son architecture QK-Norm est auto-testée au chargement comme chaque évolution de kernel.
- Llama 3.2 est réparé et de retour au catalogue : llama.cpp permute les poids d’attention à la conversion GGUF d’une façon que nos kernels n’attendaient pas. Ils sont désormais dé-permutés au chargement (toutes quantifications), et le scaling de fréquences longue-contexte « llama3 » est appliqué. Mesuré : 178 t/s de prefill, 19,5 t/s de génération.
Vrai img2img : affiner depuis les pixels
- « Affiner cette image » rejouait le même bruit initial ; désormais l’image affichée est ré-encodée en espace latent (petit encodeur VAE de 5 Mo, téléchargé à la première utilisation), re-bruitée partiellement et régénérée : la composition est conservée depuis les vrais pixels. Réglable via ?strength= (défaut 0.55).
- Une image affinée dépend de ses pixels source, donc elle n’est pas régénérable depuis prompt+seed comme les autres : elle est sauvegardée entière avec la conversation.
Sous le capot
- Le dernier kernel différé est livré : l’attention pleine (non causale) donne désormais à chaque (token, tête) un workgroup de 64 lanes avec softmax en ligne. Une passe sur les clés au lieu de deux. Auto-testé aux formes réelles du UNet au chargement, repli silencieux, ?attnfullwg=0 pour forcer l’ancien kernel.
- Vider l’historique des conversations vide maintenant vraiment l’écran (chat neuf), et l’app ne rouvre plus automatiquement une conversation dont le modèle n’est pas en cache : tu arrives sur un accueil neuf au lieu d’un chat mort.
Les longues conversations retrouvent leur vitesse (jusqu’à ×26 sur l’attention), le GPU peut se déconnecter sans tuer l’app, le modèle se charge tout seul, et l’interface assume pour de bon son identité d’imprimeur.
L’attention refaite pour le décodage : fin du 1 t/s en contexte long
- Le kernel d’attention n’utilisait qu’un thread GPU par tête : 14 threads en tout au décodage. Sur du matériel taillé pour des milliers. Au-delà de ~1 000 tokens de contexte, il devenait le mur (~680 ms par token). Les nouveaux kernels donnent à chaque tête un workgroup complet de 64 lanes avec softmax en ligne : ×14 à ×26 mesurés, résultats identiques à 1e-7 près.
- Ceinture et bretelles : au chargement, le moteur auto-teste les nouveaux kernels aux formes réelles ; un driver GPU qui les miscompile bascule en silence sur les kernels classiques (plus lents en contexte long, corrects partout). ?attndecode=0 force le repli pour diagnostiquer.
- Sur mobile, les réponses sont désormais volontairement concises (le téléphone n’a pas à chauffer une minute par réponse), et l’écran reste allumé pendant que le modèle travaille.
Un crash GPU n’est plus une fin
- Quand le système reprend le GPU (longue conversation, onglet en arrière-plan, chauffe), l’app continuait sur un device mort : statut « prêt », envois permis, chaque calcul en échec. Elle détecte désormais la perte, conserve votre conversation, et propose de vraies sorties : recharger le modèle, inspecter/vider le stockage.
- La détection WebGPU réessaie avant d’abandonner, et « Non supporté » explique désormais la cause n°1 : l’accélération matérielle désactivée dans le navigateur. Avec le réglage exact à activer.
Le modèle se charge tout seul
- Rouvrir l’app reprend votre dernière conversation ET son modèle quand il est intégralement en cache (BRIK streamés compris : desktop comme mobile, zéro réseau). Téléchargement partiel ? Le préchargement d’arrière-plan finit le travail : avec le vrai progrès affiché sur le splash de première visite, puis charge le modèle tout seul.
- Corrigé au passage : le préchargement d’arrière-plan pouvait mourir en silence avant même de démarrer, et un chargement auto au démarrage prenait le téléphone pour un desktop (f16 chargé au lieu du format mixte).
La génération d’image maigrit, et arrive sur mobile
- Le pipeline image téléchargeait 2,4 Go de poids fp16, puis les quantifiait en int8 sur votre GPU à chaque chargement. Les poids arrivent désormais pré-quantifiés et streamés par plages (repris, mis en cache) : 1,28 Go sur desktop, sortie identique. Vérifiée numériquement ET visuellement contre l’ancien chemin.
- Nouveau sur mobile (bêta) : « Essayer la génération d’image » charge SDXS-512, un UNet distillé à 1 étape, en build int4 « light » avec un encodeur de texte allégé. ~445 Mo tout compris pour une image 512px native. Jugé côte à côte contre le build lourd : quasi identique.
« Le Kern », encré jusqu’au bout
- La police de titre devient Fraunces (une serif d’imprimeur : la marque kern-B, les titres et le splash la portent), un filet rouge d’imprimeur coiffe l’app, le curseur de saisie et la sélection passent au rouge Kern, et l’écran d’accueil se lit comme une page de spécimen. Les derniers reliquats violets et dégradés ont disparu.
- Mobile désencombré : la bannière de conversion BRIK disparaît (le modèle mixte est servi automatiquement), et une seule suggestion de départ laisse la place au message d’accueil du modèle.
Le modèle mobile gagne la qualité int8 pour presque la taille int4 (nouveau format « mixte »), se télécharge tout seul en arrière-plan pendant que vous lisez l’accueil, et accueille dignement les nouveaux venus.
Quantification mixte : la qualité int8 pour (presque) la taille int4
- Une étude A/B tenseur par tenseur a montré OÙ l’int4 casse un petit modèle : le tout-int4 produit du charabia, mais garder les seules matrices d’attention en int8 restaure une qualité digne de l’int8. Le nouveau format « mixte » stocke exactement ça : corps int4 + attention int8.
- Le modèle mobile est servi au format mixte : 377 Mo au lieu de 508 (int8 plein). Pour +18 Mo par rapport à l’ancien fichier int4 qui le dégradait. Le bandeau de diagnostic affiche honnêtement « mixte int8+int4 ».
- Le profil « Mixte » est aussi proposé dans les deux convertisseurs BRIK (recommandé pour les petits modèles : l’int4 reste pour les gros).
Le téléchargement s’efface en arrière-plan
- Sur mobile, si le modèle n’est pas (entièrement) en cache, son téléchargement démarre désormais tout seul peu après votre arrivée : au moment de taper « Charger le modèle », l’essentiel est déjà local. Reprise incluse : un onglet fermé ne re-télécharge que ce qui manque.
- Visible et poli : une ligne de progression avec « Annuler », rien ne démarre si l’économiseur de données du téléphone est actif, et tout chargement réel reprend la main instantanément.
- Première visite sur mobile : un court écran d’accueil (« Préparation de votre espace IA… ») couvre le démarrage. Tap pour passer, plus jamais montré ensuite.
Sous le capot
- Le prompt n’est plus re-tokenisé de zéro à chaque message : seul le nouveau tour l’est (~×90 plus rapide sur les longs historiques).
- Les images générées ne gèlent plus la page ~100 ms après le rendu (encodage PNG asynchrone).
Mobile enfin fluide et fiable : conversations ~4× plus rapides (cache KV réutilisé entre les tours, échantillonnage sur GPU), int8 par défaut, et un panneau Réglages.
Conversations plus rapides : surtout sur mobile
- Le cache d’attention (KV) est réutilisé d’un message à l’autre : seul votre nouveau message est analysé, plus jamais tout l’historique. Avant, chaque tour relisait toute la conversation : le temps de réponse doublait dès le 2e message ; il est maintenant constant.
- Échantillonnage du prochain token calculé sur le GPU (softcap, pénalité de répétition et top-K dans la même passe que le calcul) : ~600 Ko relus par token → 512 octets. Auto-testé au chargement, avec repli automatique sur le chemin CPU si le GPU de l’appareil échoue au test.
- Boucle de génération allégée : détection de fin sur les derniers tokens seulement et affichage rafraîchi ~8×/s (au lieu d’une re-détokenisation et d’un re-rendu complets à CHAQUE token, qui asphyxiaient les téléphones). Seule la bulle en cours de frappe se re-rend.
- Résultat mesuré sur téléphone (Qwen 0.5B) : réponse complète en ~10 s au lieu de 20–38 s, génération à ~5 t/s au lieu de 2,7.
Qualité mobile : int8 par défaut
- Les petits modèles (≤ ~1,2 Md de paramètres) se chargent désormais en int8 sur mobile, plus en int4 : l’int4 dégradait fortement un 0.5B (réponses absurdes, répétitions en boucle) alors qu’il tient largement en int8. L’int4 reste réservé aux gros modèles qui ne rentreraient pas.
- Et le cache d’attention reste en f32 par défaut (plus rapide) : sa version int8. Qui ajoute du travail à chaque token : n’est activée que là où sa VRAM compte vraiment (gros modèles, int4).
- Nouveau bandeau de diagnostic sous chaque réponse : précision réelle, format du cache KV, chemin d’échantillonnage et réutilisation du contexte. Pour comprendre ce qui a tourné, même sans console. Et il dit la vérité : quand le fichier du modèle est plus quantifié que la précision demandée, il affiche par ex. « int8 (source int4) ».
- Stockage persistant demandé au navigateur : le modèle streamé mis en cache ne devrait plus être effacé entre deux sessions sur mobile.
Pipeline image : encore plus léger
- L’encodeur de texte (CLIP) rejoint le UNet : quantifié int8, résident sur le GPU, exécuté en une seule soumission. Fini les ~280 allers-retours et les ~500 Mo de poids renvoyés au GPU à chaque image.
- Chargement du modèle image sans blocage : la conversion des poids fp16 se fait désormais sur le GPU (l’onglet gelait ~10 s à chaque chargement).
- Mémoire GPU rendue après chaque image (le tampon de travail conservait des centaines de Mo), et le pipeline image est entièrement libéré (~1 Go) quand on charge un modèle de texte.
- Décodage 512px plus rapide : les convolutions 3×3 dominantes passent en kernel tuilé à mémoire partagée (chaque pixel lu 1× au lieu de 9×, chaque poids 1× au lieu de 256×), et la normalisation de groupe utilise 4× plus de threads. Les deux auto-testés au chargement avec repli automatique.
Web & outils (MCP) : premiers pas, en toute transparence
- Recherche web optionnelle (OFF par défaut) : le modèle s’appuie sur des extraits Wikipédia et cite ses sources. Seule votre question est envoyée : jamais la conversation. Signalé sous chaque réponse concernée et dans la barre de saisie.
- Calculatrice locale (ON par défaut, aucun réseau) : les calculs de vos messages sont évalués exactement côté machine et le résultat fourni au modèle. Les petits modèles se trompent systématiquement en arithmétique. Le modèle connaît aussi la date du jour.
- Lecture des liens collés (OFF par défaut) : collez une URL, le modèle lit la page (via le lecteur r.jina.ai. Indiqué sur l’option).
Réglages & correctifs
- Nouveau panneau « Réglages » dans la barre latérale : puissance GPU (Éco / Équilibré / Max. Régule la chauffe pendant la génération d’images) et les options Web & outils ci-dessus.
- Interface entièrement bilingue : la bascule FR/EN couvre désormais toute l’application (panneaux, erreurs, étapes de chargement, info-bulles…). Y compris ce changelog.
- Chargement des modèles streamés plus vif : les poids d’une couche sont récupérés en une seule requête au lieu de 9-12 (~25 requêtes au lieu de ~220 pour un modèle entier).
- L’écran de chargement devient un vrai journal : fond opaque et liste des étapes réelles (téléchargement, quantification, validation) avec leur progression.
- Le défilement automatique respecte votre lecture : si vous remontez pendant que l’IA écrit, plus aucun retour forcé en bas. Il reprend quand vous y redescendez.
- Corrigé : charger un modèle de texte par-dessus le mode image ne basculait pas. Les messages partaient en génération d’image (et le pipeline image restait en mémoire).
- Interrupteurs de diagnostic dans l’URL (?gputopk=0, ?kvreuse=0) pour isoler un souci propre à un appareil.
Génération d’images 100 % navigateur (SD-Turbo en WGSL maison), UNet quantifié int8 résident GPU, régulation thermique, et une nouvelle identité visuelle.
Génération d’images : Stable Diffusion Turbo, 100 % local
- Nouveau mode image dans le chat : décris une image, elle est générée entièrement dans le navigateur. CLIP (encodage du prompt), UNet (débruitage) et décodeur TAESD tournent sur nos propres kernels WGSL, sans aucun serveur.
- Fidélité au prompt corrigée face à la référence diffusers : padding du tokenizer (« ! », pas <|endoftext|> : sans masque d’attention, les 77 positions comptent) et LayerNorm final du CLIP appliqué. Résultat : des images cohérentes avec la demande.
- Qualité réglable par génération au-dessus de la zone de saisie : 128px (rapide), 256px (conseillé) ou 512px (résolution native de SD-Turbo).
- Conversations légères : seule une vignette floue + le prompt + la graine sont persistés ; « cliquer pour révéler » régénère l’image à l’identique (génération déterministe) sans jamais stocker les pixels.
- Poids (UNet + CLIP fp16, TAESD) mis en cache navigateur au premier chargement → les fois suivantes, aucun téléchargement.
Performance & thermique : int8 résident + régulation
- UNet quantifié int8 (BRIK8) directement sur le GPU au chargement : ~0,9 Go de VRAM au lieu de ~3,4 Go en f32, et plus aucun ré-envoi de poids à chaque image (avant : ~3,4 Go re-transférés par génération). Nouveau kernel conv2d int8 à déquantification fusionnée, couvert par le self-test.
- Exécution GPU-résidente de bout en bout : les activations restent sur le GPU entre tous les blocs (UNet et décodeur), une seule lecture CPU par étape de débruitage → beaucoup moins d’allers-retours, génération plus efficace.
- Régulation thermique intégrée : le pipeline mesure le temps GPU réellement consommé et intercale des pauses proportionnelles (~60 % de charge cible). La génération lisse sa consommation au lieu de faire chauffer la machine en rafale continue.
- Progression détaillée pendant la génération (étape de débruitage + bloc UNet en cours).
Nouvelle identité : « Le Kern »
- Exit le violet et le logo dégradé : place à une identité papier / encre / rouge imprimeur, en clin d’œil au crénage typographique de brimKERN. Mode sombre « encre » assorti.
- Logo redessiné en SVG plat (un B massif entaillé d’un kern diagonal) : net à toutes les tailles, suit le thème clair/sombre, et remplace 700 Ko de PNG.
- Favicon et carte de partage (OpenGraph) mis à jour dans la nouvelle identité.
Nettoyage
- Suppression du générateur d’image « aperçu » (placeholder), des gabarits de prompt Llama 2/3 inaccessibles depuis le retrait de Llama, et des assets inutilisés : bundle plus léger.
Gemma 2 pleinement fonctionnel, appariement tokenizer auto, reprise de conversation, perf prefill (matmul tilé), Skills, stockage enrichi et interface bilingue FR/EN.
Gemma 2: pleinement fonctionnel
- Génération cohérente sur Gemma 2 (2B) : correction d’un débordement numérique de l’activation GELU (qui produisait des NaN), et de la double application de la normalisation RMSNorm (1+w). La cause du texte incohérent.
- Appariement automatique tokenizer + architecture depuis le fichier GGUF : charger un modèle n’utilise plus par erreur le tokenizer d’un autre (un mismatch de vocabulaire donnait du charabia malgré un calcul correct).
- Socle réutilisable pour ajouter d’autres familles de modèles locaux (prochaine cible : Microsoft Phi-3.5-mini).
Sélection de modèle & stockage
- Reprise complète à l’ouverture : la dernière conversation est rechargée et, si le modèle qu’elle utilisait est déjà en cache, il est rechargé automatiquement (sans réseau) → on retombe directement sur une session prête à discuter.
- Nouveau sélecteur « Parcourir les modèles » en fenêtre large avec recherche (nom, usage, tag) et grille : plus lisible que la liste étriquée à mesure que le catalogue grandit.
- Badges sur chaque modèle : « ● en cache » (déjà téléchargé localement), « BRIK conseillé » (≥ ~1.5B : ÷2–4 la VRAM + réouvertures instantanées), et indicateur du modèle actuellement chargé.
- Panneau Stockage : modèle actif mis en évidence, badge « chargé » sur le BRIK correspondant, et bouton « Tout supprimer » (caches + BRIK + historique).
- Roadmap : aperçu de la prochaine architecture portée sur nos kernels (Microsoft Phi-3.5-mini).
Interface : sélecteur de modèle unifié
- Toute la sélection de modèle passe par une seule fenêtre « Choisir un modèle » à taille fixe (scroll interne) : onglet Modèles (grille + recherche + créateur/modalité par tuile) et onglet Importer (fichier local, URL GGUF, stream .brik), avec la case « Convertir en BRIK ».
- En-tête de chat allégé : nom du modèle raccourci (info-bulle au survol), bascule langue/thème déplacée près du logo, et options dev (précision/VRAM, cache KV, benchmark) repliées dans un accordéon fermé par défaut.
- Aperçu roadmap multimodale (Microsoft Phi-3.5, Mistral, Stable Diffusion, Qwen2-VL) en cartes « bientôt ».
Performance & conversion
- Matmul q8/q4 « tilé » au prefill : chaque invocation calcule 4 tokens d’un coup en déquantifiant chaque poids une seule fois → ~4× moins de trafic mémoire poids sur le traitement du prompt.
- Conversion GGUF → BRIK en flux (shard par shard) : pic mémoire ≈ une couche au lieu du modèle entier → de gros modèles se convertissent dans le navigateur sans saturer la RAM.
- Llama temporairement retiré des architectures proposées (incompatibilité RoPE / poids permutés par llama.cpp → sorties incohérentes) ; il reviendra avec un mode RoPE adapté. Architectures actives : Qwen 2/2.5, Gemma 2, DeepSeek-R1 (distill Qwen).
Skills : consignes réutilisables
- Bibliothèque de « skills » (personas / instructions système) : intégrés + tes propres skills, persistés localement.
- Multi-sélection (les skills se combinent), import depuis une URL GitHub, et popup accessible via un bouton à gauche de la barre de chat.
Gestion du stockage
- Panneau « Stockage » : voir l’espace pris par les modèles streamés, GGUF téléchargés, BRIK convertis et l’historique. Avec suppression individuelle.
Mobile & bilingue
- Mobile simplifié : un seul modèle prêt à l’emploi (Qwen 0.5B BRIK streamé), barre de progression de téléchargement, et la zone de chat s’affiche pendant le chargement.
- Interface bilingue : français (défaut) / anglais, bascule dans l’en-tête, mémorisée.
BRIK autonome : modèles hébergés, tokenizer embarqué, fichiers plus légers, et chargement mobile optimisé.
BRIK autonome, hébergé & optimisé mobile
- Tokenizer embarqué dans le .brik → chargement 100 % hors-ligne, sans fetch externe ni choix manuel de tokenizer.
- Embeddings « tied » dédupliquées (output = token_embd) → fichier ~⅓ plus léger, sans perte de qualité.
- Modèle Qwen 2.5 0.5B pré-converti, hébergé et streamé par HTTP Range (faible VRAM) → chargement direct optimisé, mobile comme desktop.
- Conversion GGUF → BRIK fiabilisée : les très gros tenseurs sont traités par tranches pour ne plus dépasser la limite de buffer GPU (qui corrompait silencieusement les poids).
- Niveau de réflexion réglable (off / low / medium / high) pour les modèles de raisonnement ; catalogue resserré sur les architectures pleinement supportées (Qwen, Gemma, DeepSeek).
Format BRIK v2 : plus léger & streamable
- Quants web-natifs BRIK8 (int8) et BRIK4 (int4) : déquantification fusionnée dans le matmul GPU (poids gardés quantifiés en VRAM), petit téléchargement ET inférence rapide.
- Fichier unique .brik auto-contenu (en-tête + manifeste + données alignées 16 octets) à la place du .brik.zip : un seul fichier à héberger/charger.
- Chargement par streaming HTTP Range : seul l’en-tête (manifeste) est récupéré d’abord, puis chaque tenseur à la demande, mis en cache (Cache API) → re-chargements instantanés et hors-ligne.
- Embeddings stockés en int8 (souvent le plus gros tenseur) → téléchargement et VRAM nettement réduits, qualité quasi inchangée.
Contexte plus long: cache KV q8
- Cache attention (K/V) optionnel en int8 : ÷~4 la VRAM du cache → jusqu’à ~4× plus de contexte à VRAM égale, qualité quasi-f16.
- Bascule « KV f32 / KV q8 » dans le réglage de précision (déquant fusionnée dans l’attention, sans expansion f32).
Nouvelles architectures
- Support de Gemma 2 par les kernels optimisés : softcap d’attention + des logits, activation GELU, doubles normes « sandwich », RMSNorm (1+w), scaling des embeddings, head_dim ≠ d/têtes.
- Kernels rendus paramétrables (scale d’attention, activation, normes) : fondation réutilisable pour d’autres familles.
Chargement & cache
- Conversion automatique GGUF → BRIK au chargement (optionnelle) : faite une seule fois, puis le .brik converti est mis en cache (IndexedDB) pour des ouvertures instantanées.
- Page de conversion dédiée avec gestion du cache des modèles convertis (lister / supprimer).
- Précision des poids clarifiée : GGUF en f16/f32, tiers BRIK8/BRIK4 réservés aux modèles BRIK.
Robustesse & correctifs
- Correction d’un débordement de dispatch GPU sur les longs prompts (> ~860 tokens) qui produisait une sortie incohérente : désormais réparti sur une grille 2D.
- Compteur de tokens + avertissement de contexte dans le composer.
- Gros collage replié en « extrait » dans le champ de saisie (le texte complet reste envoyé au modèle).
- Correctifs d’interface : plus d’anneau de focus résiduel au clic, bulles de message qui ne coupent plus les mots courts.
Première version : un moteur d'inférence LLM écrit de zéro, 100 % dans le navigateur.
Moteur WebGPU custom
- Kernels de calcul WGSL maison : matmul vectorisé (vec4 128-bit), RMSNorm, RoPE, attention causale GQA avec cache KV, SwiGLU.
- Parsing GGUF directement en JavaScript et déquantification des poids sur le GPU.
- Chemin de décodage « GPU-resident » : tout le passage avant d'un token enchaîné en une seule soumission GPU.
- Auto-validation des kernels au chargement (selfValidate) : le modèle ne se charge que si les calculs sont corrects.
Performances
- Projection des logits mise en cache sur le GPU au lieu d'être recalculée à chaque token.
- Argmax du token suivant calculé sur le GPU (un seul entier relu par token au lieu de ~152k logits).
- Pool de buffers réutilisés entre les tokens. Résultat global : décodage ~2,5× plus rapide.
Précision des poids (commutable)
- f32 : pleine précision (référence qualité).
- f16. Demi-précision : ~1,25× plus rapide, ½ de la VRAM (selon le GPU).
- int4 (format BRIK « q4web ») : déquantification à la volée, ¼ de la VRAM → permet de charger des modèles plus gros dans le navigateur.
Modèles & quantifications
- Modèles : Qwen 2.5 (0.5B, Coder 1.5B), Llama 3.2 1B, DeepSeek-R1 Distill Qwen 1.5B (raisonnement <think>).
- Quantifications GGUF lues : Q4_0, Q4_K, Q5_0, Q5_K, Q6_K, Q8_0, F16, F32.
- Import de n'importe quel GGUF compatible (fichier local ou URL Hugging Face).
Interface
- Chat avec rendu Markdown (gras, italique, listes, titres) et coloration + copie des blocs de code.
- Historique des conversations persistant (IndexedDB), indépendant du modèle chargé.
- Benchmark intégré comparant f32 / f16 / int4, et sélecteur de précision.
- Panneau latéral repliable, interface adaptée mobile.
Confidentialité
- Aucune donnée envoyée à un serveur : modèle et calculs s'exécutent entièrement sur votre GPU, hors-ligne après le téléchargement du modèle.
Brimkern : moteur WebGPU open, conçu par Romain Khanoyan. IA locale, WebGPU, moteurs on-device.