Un RNN de 144M peut-il rivaliser avec les meilleurs embedders transformers ?.

14 juillet 2026

TL;DR


1. Le contexte : pourquoi l'embedder est l'organe vital d'un RAG strict

Morrigan, notre assistant IA souverain, repose sur un pari : zéro hallucination structurelle. Sans contexte documentaire jugé fiable, le modèle de génération n'est jamais appelé, et le système répond « je ne sais pas ». Ce pari fait porter tout le poids sur le retrieval. Un embedder médiocre ne dégrade pas les réponses : il les supprime (refus à tort) ou les empoisonne (mauvais contexte présenté comme fiable).

Le cas d'usage est exigeant : des questions en français sur de la documentation technique en anglais (man pages, MDN, doc Python). Du cross-lingue, du jargon, des corpus fédérés.

Et nous avions une contrainte de thèse : Morrigan génère avec RWKV-7, un RNN, pas un transformer. Coût linéaire, état constant, 100 % CPU. La question qui fâche : le retrieval pouvait-il, lui aussi, tourner sur cette architecture sans sacrifier la qualité ?

2. Le shootout : établir qui est le meilleur (ce n'était pas nous)

Méthode : sous-ensemble équitable de 5 000 chunks de notre corpus de production (46 569 au total), passages pertinents garantis présents, mêmes 56 requêtes FR pour tous les modèles, hit@k comparable. Le jeu d'éval est versionné et n'a jamais servi à entraîner quoi que ce soit.

modèleparamsdimslicencehit@1hit@3vitesse CPU¹
granite-107m107M384Apache-2.03953/56~2,7 ch/s
granite-278m278M768Apache-2.04155/56<0,7 (lent)
multilingual-e5-large560M1024MIT4150/56~0,9
multilingual-e5-small118M384MIT3348/56la + rapide
embeddinggemma-300m300M768Gemma3748/56~2,4
multilingual-e5-base278M768MIT3246/56~2,2
EmbeddingRWKV 0.4B389M768Apache-2.02330/56~1,4
EmbeddingRWKV 0.1B144M768Apache-2.02226/56~4

¹ chunks/seconde sur i5-10210U (laptop) ; l'absolu dépend du throttling, le relatif est fiable.

Verdict sans appel : granite-107m offre le meilleur rapport précision/poids/vitesse de sa catégorie, à 2 hits seulement du 278m qui coûte 2,6 fois son poids et reste inutilisable en CPU. Il est passé en production. Le leader de la classe 100-150M, c'est lui.

Et l'embedder RWKV ? 26/56 pour le 0.1B (mesuré avec l'instruction de requête recommandée, le défaut de notre moteur ; 18/56 sans), à 22-27 hits du haut du tableau. Le papier d'origine (EmbeddingRWKV, arXiv:2601.07861) annonçait pourtant un MTEB anglais de 63.06, à 0,8 point d'un transformer de même taille (gte-base : 63.90). Mais le multilingue n'y était pas évalué : notre bench était, à notre connaissance, la première mesure cross-lingue de ce modèle. Conclusion provisoire : écarté.

3. Le rebondissement : le 26/56 était un artefact de mesure

Une ligne de bas de page de notre propre doc de bench disait : « RWKV évalué à cap 256 tokens (concession de temps CPU) vs 512 pour les autres ; si RWKV est proche, re-run fair-512 recommandé ». À 26/56, personne n'y croyait. On l'a fait quand même.

EmbeddingRWKV 0.1Bhit@1hit@3gate²
cap 256 tokens2226/560,70
cap 512 (équitable)3848/560,911

² gate = balanced accuracy de séparation in-corpus / hors-corpus ; notre mesure du « sait dire je ne sais pas ».

+22 hits en levant le cap. L'explication tient à l'architecture. Un transformer en mean-pooling moyenne tous les tokens : tronquer le passage lui coûte peu. Un RNN causal poole son état final, construit de gauche à droite : tronquer à mi-passage ampute l'embedding de la moitié du contenu. Leçon d'évaluation qui dépasse notre cas : un cap de troncature « raisonnable » peut écraser une architecture récurrente de 22 points sans qu'aucune erreur ne soit visible nulle part.

À armes égales, le RNN de 144M faisait donc jeu égal avec e5-small et embeddinggemma-300m, battait e5-base (278M), et se tenait à 5 hits de granite. La campagne devenait jouable.

4. La campagne : cinq runs, une hypothèse par run, 0 €

Règles fixées à l'avance : une seule hypothèse par run (jamais deux changements à la fois), le bench local comme juge unique, tous les checkpoints conservés, les échecs documentés. Compute : le tier gratuit de Kaggle (T4, ~30 h/semaine) pour l'entraînement, le laptop pour l'inférence et les verdicts. Fine-tune léger : tête retrieval plus les 2 derniers blocs (16,3M paramètres entraînés sur 144M).

Round 1, l'échec instructif (données FR→FR)

60 000 paires du config « french » de mMARCO plus 20 000 CodeSearchNet, InfoNCE in-batch. Résultat : 47/56 (best), 43/56 (final), une régression sous la baseline de 48, qui s'aggrave avec la durée d'entraînement (48, puis 47, puis 43 : une courbe dose-réponse propre).

Le diagnostic tenait en une ligne : le config « french » de mMARCO fournit des paires FR→FR. Notre bench mesure du FR→EN. Nous avons entraîné le modèle à être monolingue, et il a consciencieusement désappris le cross-lingue. Deuxième faille : 8 paires par batch, soit 7 négatifs par positif ; la perte s'effondrait à ~0,005 en 300 pas, plus aucun signal.

Round 2, le fix causal (vraies paires FR→EN)

Les traductions mMARCO étant alignées par identifiants, zipper le flux « french » et le flux « english » donne des paires (requête FR, passage EN) exactes. 50 000 paires FR→EN, 10 000 FR→FR de rétention, 10 000 CodeSearchNet, batch de 24 (23 négatifs). Une époque.

Résultat : hit@1 39, parité avec granite, hit@3 48, gate 0,920.

Round 2b, l'autre échec instructif (une époque de trop)

Mêmes données, deuxième époque. Le score interne de validation montait encore (0,902 à 0,910), et le bench a reculé : 36 hit@1, 47 hit@3. Sur-spécialisation au style mMARCO, dérive de la cible. Loi de campagne n° 4 : sur ces volumes, une époque, pas davantage. Un proxy de validation qui monte ne prouve rien sur la compétence visée.

L'autopsie : savoir exactement quoi attaquer

Avant le round final, nous avons disséqué les 8 échecs restants du hit@3 et croisé avec les échecs de granite sur les mêmes requêtes :

Round 3, la fabrique de requêtes synthétiques

Le vocabulaire de domaine ne s'apprend pas dans des paires web généralistes. Nous avons donc généré nos données : 1 052 requêtes françaises synthétiques produites par un LLM ouvert que nous exécutons nous-mêmes (Qwen2.5-7B, sur le GPU gratuit) à partir des chunks de notre propre corpus. Environ 600 chunks visés (400 ciblés sur les domaines des 6 échecs, 200 tirés au hasard), dont 547 ont produit des requêtes retenues, avec une garde anti-fuite stricte : toute requête générée trop proche d'une requête du jeu d'éval est jetée (recouvrement Jaccard supérieur à 0,5 = poubelle), zéro fuite vérifiée.

Recette finale : requêtes domaine ×3, plus 8 000 paires FR→EN de rétention, départ depuis le champion du round 2, une seule passe, learning rate doux.

5. Les résultats

Sur notre bench (le domaine de production)

paramshit@1hit@2³hit@3gate
RWKV baseline (fair-512)144M38480,911
RWKV round 2144M39480,920
RWKV round 3 (final)144M4347510,938
granite-107m107M3947530,932
granite-278m278M41550,940
e5-large560M41500,92

³ hit@2 = la métrique de production : Morrigan n'injecte que les 2 premiers chunks dans le prompt de génération.

Lecture :

Sur juge externe (MIRACL-fr, jamais vu à l'entraînement)

343 requêtes du dev set MIRACL français, pool de 5 000 passages Wikipédia (731 passages-or garantis, distracteurs échantillonnés uniformément sur les 14,6M du corpus). Domaine généraliste, monolingue FR : tout ce que notre fine-tune n'a pas visé.

hit@1hit@2hit@3hit@10
granite-107m97,7 %99,1 %99,4 %99,7 %
RWKV round 395,6 %98,3 %98,8 %99,7 %
RWKV baseline95,9 %99,1 %99,4 %99,7 %

Deux réponses dans ce tableau :

  1. L'adaptation domaine n'a rien cassé : moins 0,3 point de hit@1 par rapport au modèle d'origine, soit une requête sur 343. La tranche de rétention FR→EN dans le mix d'entraînement a fait son travail.
  2. En généraliste, granite garde ~2 points. Le RNN est compétitif hors de chez lui et dominant chez lui. C'est exactement le profil qu'on attend d'une adaptation réussie, et le compromis est mesuré, pas supposé.

À ce stade, tout disait : basculez. Deux juges sur deux, la parité en production, la rétention intacte. C'est là qu'on a exigé un troisième juge. Il a tout changé.

6. Le juge de trop : la pleine échelle inverse le verdict

Tous les chiffres précédents venaient d'évals en pool : 5 000 passages, pertinents garantis, distracteurs échantillonnés. Avant de toucher à la production, nous avons imposé une porte de bascule : le même duel, mais sur les stacks complètes : 46 569 chunks de code plus les trois corpus système, retrieval fédéré, gates de production, zéro échantillonnage.

stack complète (46k+)hit@1hit@2hit@3faux refusrefus hors-corpus
granite-107m3744495/5621/24
RWKV round 33540449/5620/24

L'avantage du subset s'est inversé. Tout le monde descend en passant de 5 000 à 46 000 candidats, mais pas à la même vitesse : granite perd 2 à 4 hits, le RNN en perd 7 à 8. Et il refuse à tort presque deux fois plus (9 contre 5).

Le mécanisme, une fois vu, est limpide. Le fine-tune a appris l'attraction (rapprocher la requête du bon document), et ça tient à toute échelle. Il n'a jamais appris la séparation : nos batchs contrastifs opposaient chaque paire à 23 négatifs aléatoires, jamais aux quasi-voisins qui pullulent dans un vrai corpus (les 40 pages MDN sur les fonctions, les 15 man pages réseau presque identiques). Dans un pool de 5 000, peu de sosies : l'attraction suffit. Dans 46 000, les sosies décident du classement. Et là, l'espace « affûté » de granite, forgé par IBM sur des volumes massifs avec négatifs durs, fait la différence. Un facteur aggravant : la distribution de cosinus du RNN est resserrée (~0,2-0,6 contre ~0,3-0,9), moins de plage dynamique pour départager les quasi-égalités.

Décision : pas de bascule. Notre règle, écrite avant la campagne, disait : la thèse gagne les égalités et les petites défaites, pas les nettes. Celle-ci est nette. granite reste en production. Le protocole a fait exactement ce pour quoi il existait : nous empêcher de basculer sur de beaux chiffres de pool.

Ce résultat est aussi, à notre connaissance, une donnée nouvelle : les évaluations en pool surestiment systématiquement un embedder RNN relativement à un transformer. L'écart de sensibilité à l'échelle n'apparaît qu'en conditions réelles. Si vous benchez des architectures différentes sur des pools réduits, ce biais vous concerne.

Restait une carte à jouer, la bonne : un round 4 aux négatifs durs minés à pleine échelle, entraîner le modèle contre ses propres sosies, exactement la technique qui a forgé les embedders de référence. Mais en préparant ce round, on a d'abord trouvé autre chose.

7. L'interlude : le bug qui effondrait tous les textes longs sur un même point

Une piste extérieure nous met la puce à l'oreille : comment gérons-nous le token de fin de séquence (EOS) à la troncature ? Vérification faite, notre préprocessing (portage CPU et lib d'entraînement) encodait avec EOS puis tronquait au cap. Tout texte plus long que le cap perdait donc son EOS à la coupe.

Or le pooling d'EmbeddingRWKV ne lit l'état caché qu'aux positions EOS. EOS absent → masque de pooling vide → vecteur nul → la tête projette le même embedding constant pour tous les textes trop longs. Aucune erreur, aucun NaN, aucun avertissement. Mesure : cosinus de 1,0000 entre un texte long de code Python et une recette de tarte. Après correction (tronquer à max_tokens − 1 puis ajouter l'EOS) : 0,159.

L'ampleur, mesurée sur des échantillons de 400 chunks par corpus :

cap de troncatureusagetextes touchés (EOS perdu)
512 tokensinférence / indexation0,5 % des chunks (~250 sur 46 569 au point constant)
320 tokensentraînement Kaggle19 à 25 % des passages selon le corpus

Autrement dit : un passage d'entraînement sur cinq des rounds 1 à 3 n'apportait aucun signal, ou pire, attirait la requête vers le point constant. Le fair-512 du §3 s'explique d'ailleurs en partie par ce mécanisme : monter le cap de 256 à 512 sortait du point constant tous les chunks de 257 à 512 tokens. Les données minées du round 4 étaient, elles, saines (0 négatif sur 6 292 au-delà du cap d'indexation, vérifié).

Le fix fait deux lignes ; il est publié dans le portage CPU et signalé sur le fil de discussion du modèle. La leçon dépasse largement notre cas : un embedder à pooling d'état terminal ne dégrade pas gracieusement à la troncature : il s'effondre en silence. Un transformer mean-pooling perd de l'information ; un RNN qui lit son état à l'EOS perd tout, d'un coup, sans le dire. Si vous tronquez, vérifiez que votre token de lecture survit à la coupe.

8. Round 4 : les négatifs durs face au juge le plus dur

Le round 4 attaque le mécanisme du §6 : chaque requête s'entraîne contre ses propres sosies, les 6 documents qui la piégeaient réellement, minés parmi les erreurs top-k du champion sur la stack complète. En chiffres : 1 052 requêtes domaine × 6 négatifs durs (6 292 au total), chaque paire vue 3 fois avec des tirages de sosies différents, plus 8 000 paires FR→EN de rétention (7 744 à l'entraînement, 256 en validation pure). Perte InfoNCE à négatifs explicites : batchs de 12 paires × (1 positif + 3 négatifs durs échantillonnés) = 60 séquences par pas, ~908 pas, une seule passe, LR 1e-5, toujours 16,3M paramètres entraînés, avec le préprocessing corrigé du §7.

Note de méthode, à la loyale : notre juge pleine échelle d'origine a été perdu entre-temps (un script resté dans un répertoire temporaire, un redémarrage ; un juge est du code : versionnez-le). Il a été reconstruit en plus strict : les hits comptés sur la recherche fédérée brute, le verdict de refus rendu par le vrai gate de production (seuil cosinus + garde cross-langage + garde lexicale), le seuil de chaque modèle calibré par balanced accuracy sur ses propres scores. Les trois prétendants ont été re-jugés dans des conditions strictement identiques : stacks complètes de production, 49 795 chunks (46 569 code + 1 988 sysadmin + 626 sécurité + 612 réseau). Les chiffres qui suivent ne sont donc pas comparables ligne à ligne avec ceux du §6. Entre eux, ils le sont parfaitement.

pleine échelle, juge reconstruithit@1hit@2⁴hit@3faux refusrefus hors-corpusgate calibré
granite-107m (production)41/5647/5648/562/5617/240,64
RWKV round 326/5638/5642/567/5622/240,498 (bacc 0,887)
RWKV round 430/5638/5640/565/5622/240,404 (bacc 0,869)

hit@2 = la métrique de production (2 chunks injectés dans le prompt de génération).

Trois lectures, dans l'ordre :

  1. Les négatifs durs fonctionnent. À juge identique, le round 4 gagne +4 en hit@1 sur le round 3 et réduit les faux refus de 7 à 5. L'autopsie confirme le mécanisme : les échecs restants se concentrent sur les zones que les 1 052 requêtes domaine ne couvraient presque pas (man pages, SQL ; la génération ciblait surtout MDN, 544 requêtes contre 49 pour les man pages). La technique marche ; c'est la couverture qui manquait.
  2. L'écart avec granite est structurel. −11 en hit@1, −9 sur la métrique de production. À taille quasi égale (144M contre 107M), c'est l'entraînement massif multilingue d'IBM qui parle, pas la taille. Aucune vague réaliste de fine-tune à 16M de paramètres entraînés ne comble ça. Les leviers restants (backbone entier, distillation, le 0.4B jamais évalué proprement) sont des paris de recherche, plus une course à la bascule.
  3. Le RNN repart avec une couronne : le refus. 22 requêtes hors-corpus sur 24 refusées, contre 17 pour granite, qui laisse passer presque une question hors-sujet sur trois vers le générateur. Sur le « sait dire je ne sais pas », le pari fondateur de Morrigan, le petit RNN fine-tuné à 0 € bat nettement le transformer de référence. Les distributions de scores racontent la même histoire : après négatifs durs, le hors-corpus plafonne à 0,62 quand le pertinent monte à 0,83 : le modèle a appris à creuser l'écart.

Verdict final : granite reste en production. La règle écrite avant la campagne a tranché deux fois dans le même sens, et c'est très bien ainsi : un protocole qui ne peut dire que oui n'est pas un protocole. La campagne se clôt ici. Le programme, lui, continue, à raison d'une hypothèse par mois : le backbone entier jamais entraîné, la distillation jamais tentée, le 0.4B jamais testé dans des conditions propres, et un embedder multilingue généraliste pour la communauté, avec exactement cette recette.

9. Ce que ça coûte, ce que ça change

10. Les limites, sans maquillage

11. Reproduire

Remerciements et crédits

Ce travail se tient sur les épaules d'EmbeddingRWKV (howard-hou) et de l'architecture RWKV-7 « Goose » (BlinkDL et la communauté RWKV). Les comparaisons utilisent les excellents granite-embedding (IBM), multilingual-e5 (Microsoft) et EmbeddingGemma (Google). Données : mMARCO (UNICAMP), CodeSearchNet (GitHub), MIRACL. Génération synthétique : Qwen2.5 (Alibaba). Compute : tier gratuit Kaggle.


Morrigan fait partie de l'écosystème ScarletWolf : des outils IA souverains, conçus pour tourner chez vous, sous votre contrôle. Le détail de la campagne, y compris tout ce qui n'a pas marché, est dans nos logs. Un résultat sans ses échecs est une publicité, pas un résultat.

Une question, un désaccord, une envie d'essayer ? Écrivez-moi, c'est le fondateur qui répond.