On descend d'un cran

Depuis plusieurs mois, je travaille essentiellement sur deux projets.
Herbert, mon moteur d'inférence, fait tourner des modèles de langage en local.
Le petit dernier de la famille mAIstrow, mAIrness, est le harnais qui fait travailler ces modèles sur de vrais projets, sans leur donner les clés de la machine. Disons que c'est un peu le Claude Code ou le Codex qui vient avec Herbert.
Ce qui rend le développement des deux possible en même temps, c'est leur rythme. Une campagne de mesure sur Herbert dure des heures, parfois plusieurs jours. Une étude sur mAIrness, c'est pareil : elle enchaîne des dizaines de runs d'agent.
Pendant que l'une des campagnes tourne, je travaille sur l'autre projet.
Après un travail acharné pendant tout l'été 2026, j'arrive enfin à un point où je peux communiquer sur ces deux projets.
Dans les semaines à venir, je vais publier les résultats et des commentaires sur mon travail. Considérez chaque article comme le journal de ce qui a avancé dans la semaine.
On verra le harnais dès que possible. Il est utilisé au quotidien par quelques personnes de confiance, mais il n'est pas assez costaud pour une diffusion large.
Alors pour commencer, je vais vous parler de ce qui a changé avec Herbert.
Le moteur d'inférence qui écrit son propre code
Le 15 mars 2026, je présentais ici herbert-rs, un moteur dont presque chacune des routines était écrite à la main, en assembleur.
Depuis, j'ai changé de stratégie, pour deux raisons que j'ai mis du temps à accepter.
La première est à imputer aux compilateurs Rust. Quand on écrit un noyau de calcul avec des intrinsèques, c'est-à-dire des fonctions proches des instructions disponibles en assembleur, le compilateur garde le droit de réordonner et de fusionner. Il lui arrive même de réécrire une variante en une autre sans le dire. J'ai eu un banc qui concluait que l'essentiel du coût venait d'un endroit précis.
Le même banc, avec un code que je contrôlais instruction par instruction, disait le contraire. Deux mesures identiques, aucune alerte : il a fallu désassembler pour comprendre. Et du temps, beaucoup trop de temps.
La seconde tient à l'assembleur lui-même. Écrit à la main, il règle ce problème. C'est pour ça que j'avais fait ce choix. Mais ce code est figé. La forme d'une couche, le nombre de têtes, la largeur des registres et les instructions disponibles, je ne les connais vraiment qu'au démarrage, devant le modèle et devant la machine. Il me fallait juste une sorte de template qui s'adapte aux conditions d'exécution.
Voilà pourquoi, maintenant, avec cette nouvelle version de Herbert, le code s'écrit donc au démarrage. C'est mon programme qui décide de chaque instruction et c'est lui qui la pose. Je ne mesure plus une instruction que je n'ai pas le droit d'émettre. Dites-vous que ça ressemble à une sorte de compilateur pour moteur d'inférence.
La version que je vais publier fait un peu plus de 40k lignes de Rust. Elle ne contient aucun fichier assembleur, aucune fonction intrinsèque et pas une ligne de C.
Ce que fait le moteur
Pour ce moteur, un modèle de langage travaille dans deux régimes. Le prefill, quand il lit la question et le décodage, quand il écrit la réponse, un jeton après l'autre.
Pour chacun des deux, Herbert assemble un seul programme. Tous les kernels nécessaires y sont : les projections, les normes, l'attention, le mixer Gated DeltaNet, la tête de sortie.
Et puis il y a la gestion des threads, pour profiter des cœurs du processeur sur lequel le moteur fonctionne. Chaque thread exécute ce programme d'un bout à l'autre. Des barrières à l'intérieur du programme les gardent au même pas. C'est un choix sur lequel je reviendrai. Mais en gros, il n'y a pas un ordonnanceur d'un côté et les exécuteurs de l'autre.
Rust ne fait que trois choses : charger les poids, émettre le code, tenir la conversation.
Ce qu'il y aura dans le dépôt
Au début, un seul modèle, Qwen3.5 en texte. Un seul contrat numérique : les poids, les activations et le cache sont en bf16, les accumulateurs en f32. Rien n'est quantifié.
Trois cibles :
- x86-64, Linux et Windows : AVX2 et FMA ; les noyaux larges avec l'AVX-512 BF16
- AArch64, les processeurs Apple Silicon sous macOS : NEON et BF16 ; SME2 quand il existe et même l'AMX d'Apple en option
- RISC-V, Linux : RVV 1.0 à 256 bits au moins, avec les extensions bf16
Je ne vous en dis pas plus pour le moment, mais on va se régaler.
Lire ce que le moteur a écrit
Une option écrit sur le disque le code que le moteur vient d'émettre. Elle s'emploiera ainsi :
cargo build --release target/release/herbert-bf16-chat ~/models/Qwen3.5-4B --dump-asm listings
Voici la même opération, une projection de la première couche au décodage, telle que le moteur l'a écrite sur deux machines. D'abord sur un processeur AMD avec l'AVX-512 :
mov rcx, 0x500 prefetcht0 [rsi+0x40] prefetcht0 [rsi+0x14040] vmovdqu16 zmm2,[rsi] vmovdqu16 zmm3,[rsi+0x14000] vpbroadcastd zmm4,[rdi] vdpbf16ps zmm0,zmm4,zmm2 vdpbf16ps zmm1,zmm4,zmm3 add rdi,4 add rsi,0x40 dec rcx jne short 0x135
Puis sur un Mac, avec NEON :
movz x15, #0x140 ldr q0, [x0], #16 ldp q4, q5, [x1], #32 ldp q6, q7, [x1], #32 bfmlalb v16.4s, v4.8h, v0.h[0] bfmlalt v16.4s, v4.8h, v0.h[1] bfmlalb v17.4s, v5.8h, v0.h[0] bfmlalt v17.4s, v5.8h, v0.h[1]
Regardez la première ligne de chaque extrait. Le nombre de tours de la boucle est écrit en dur dans le code : 0x500 d'un côté, 0x140 de l'autre. Les décalages vers les poids aussi. Le moteur connaissait la forme de la couche au moment d'écrire, il n'a donc rien à recalculer pendant qu'il tourne.
Quand le code sera en ligne, vous pourrez refaire ces listings chez vous et les comparer aux miens.
Ce qui n'y sera pas tout de suite
La version quantifiée sur 4 bits d'Herbert, le Q4, tourne en production chez des clients. Je considère qu'elle n'est pas encore publiable : je veux la même qualité de mesure que pour le bf16 avant de la sortir.
Les versions Metal et Vulkan existent aussi et sont fonctionnelles. Elles viendront plus tard, dans leur propre dépôt. Je ne sais pas s'il y aura une version CUDA. J'en doute. Mais on verra bien.
Je ne touche plus à herbert-rs pour le moment. Je l'archiverai quand les autres versions seront disponibles. herbert-jit-bf16 deviendra la référence. Les autres modèles y arriveront.
Pourquoi commencer par le bf16
C'est le Q4 qui est en production. C'est pourtant le bf16 que je publierai en premier. Sans quantification, il n'y a plus de variable cachée : quand un chiffre bouge, c'est le calcul ou c'est la mémoire. La quantification viendra après. Ces mesures serviront à dire où elle se justifie.
La règle du jeu
Je l'écrivais déjà en mars : toute affirmation technique repose sur des mesures reproductibles. Pour cette série, ça veut dire quatre choses.
- Le code est cité par son tag, jamais par sa branche principale, qui va bouger.
- Les autres moteurs sont cités par leur version exacte.
- Les scripts de mesure et les données brutes sont publiés avec l'article.
- Mes prédictions sont écrites avant les mesures, datées par un commit.
Si un chiffre vous semble faux, vous aurez de quoi le vérifier.
La suite
Le prochain article ouvre les mesures. Rien n'y sera écrit que vous ne puissiez rejouer.