Reverse Engineering - Mars 2026 Reverse Engineering - March 2026

Comment j'ai reverse-engineeré le modèle IA caché de Chrome How I reverse-engineered Chrome's hidden AI model

D'un binaire opaque de 4 Go à 847 tenseurs, un gain de perplexité de 15 870x, et un modèle qui ne fonctionne toujours pas 😅 From a 4 GB opaque binary to 847 tensors, a 15,870x perplexity gain, and a model that still doesn't work.

4 GB
Fichier binaire brutRaw binary file
847
Tenseurs extraitsTensors extracted
270+
Scripts écritsScripts written
55+
Hypothèses testéesHypotheses tested
15,870x
Gain de perplexitéPerplexity gain
0.9999+
Cosim vérificationVerification cosim

Chrome ships a full large language model to every user's machine. No server call, no API, no documentation. I extracted 847 tensors from a headerless 4 GB binary, verified each at cosine similarity 0.9999+, and assembled a GGUF that runs in llama.cpp without warnings. The output is garbage. The bytes were correct. The interpretation was wrong.

Chrome embarque un modèle de langage complet sur la machine de chaque utilisateur. Pas d'appel serveur, pas d'API, pas de documentation. J'ai extrait 847 tenseurs d'un binaire brut de 4 Go sans en-tête, vérifié chacun à une similarité cosinus de 0.9999+, et assemblé un GGUF qui tourne dans llama.cpp sans avertissement. La sortie est du charabia. Les octets étaient corrects. L'interprétation était fausse.

think.resoneo.com

Powered by RESONEO

Chrome ships a full large language model to every user's machine. The model powers "Help me write," tab summarization, and the Prompt API. It weighs 4 GB, sits in a flat binary file called weights.bin, and contains no header, no magic bytes, no table of contents. I set out to extract it, understand it, and make it run outside Chrome. The output is garbage. But the kind of garbage changed over time, and each change taught me something.

Chrome embarque un modèle de langage complet sur la machine de chaque utilisateur. Le modèle alimente "Help me write", le résumé d'onglets et la Prompt API. Il pèse 4 Go, réside dans un fichier binaire brut appelé weights.bin, et ne contient ni en-tête, ni magic bytes, ni table des matières. J'ai entrepris de l'extraire, de le comprendre, et de le faire tourner en dehors de Chrome. La sortie est du charabia. Mais le type de charabia a changé au fil du temps, et chaque changement m'a appris quelque chose.

ContextContexte

What is Gemini Nano v3?Qu'est-ce que Gemini Nano v3 ?

Every Chrome installation quietly downloads a 4 GB file containing a full language model, the same kind of neural network that powers chatbots like ChatGPT or Google's own Gemini. Except this one runs entirely on your machine. No server call. No internet required. No data leaves your browser.

Chaque installation de Chrome télécharge discrètement un fichier de 4 Go contenant un modèle de langage complet, le même type de réseau de neurones qui fait fonctionner les chatbots comme ChatGPT ou le Gemini de Google. Sauf que celui-ci tourne entièrement sur votre machine. Pas d'appel serveur. Pas d'internet requis. Aucune donnée ne quitte votre navigateur.

Google calls it Gemini Nano v3. Internally, it's codenamed nano2. It has 35 layers, 2 billion parameters, and understands 250,100 tokens. The architecture supports up to 4,096 tokens of context, enough for a few pages of text, not enough for a legal contract.

Google l'appelle Gemini Nano v3. En interne, son nom de code est nano2. Il comporte 35 couches, 2 milliards de paramètres, et comprend 250 100 tokens. L'architecture supporte jusqu'à 4 096 tokens de contexte, assez pour quelques pages de texte, pas assez pour un contrat juridique.

What it does today in Chrome: "Help me write" (right-click any text field, Chrome drafts a reply or rewrites your text), tab summarization (generates a short summary of the page you're reading), scam detection (flags suspicious pages and permission prompts in real time), and the Prompt API (lets any website run AI queries locally, with no API key and no cloud cost).

Ce qu'il fait aujourd'hui dans Chrome : "Help me write" (clic droit sur un champ texte, Chrome rédige une réponse ou reformule votre texte), résumé d'onglet (génère un résumé court de la page consultée), détection d'arnaques (signale les pages suspectes et les demandes de permissions en temps réel), et la Prompt API (permet à n'importe quel site web d'exécuter des requêtes IA localement, sans clé API et sans coût cloud).

What it does not do: the Gemini sidebar (the chatbot panel you can open in Chrome) runs on Google's servers using a much larger model. The "Skills" features (buying advice, flashcard generation, review summaries) also run server-side. Nano handles the fast, private, low-stakes tasks. The heavy lifting stays in the cloud.

Ce qu'il ne fait pas : la barre latérale Gemini (le panneau chatbot que l'on peut ouvrir dans Chrome) tourne sur les serveurs de Google avec un modèle bien plus gros. Les fonctions "Skills" (conseils d'achat, génération de flashcards, résumés d'avis) tournent aussi côté serveur. Nano gère les tâches rapides, privées, à faible enjeu. Le gros du travail reste dans le cloud.

The model is a completion engine, not a chatbot. It has no concept of "turns" in a conversation. Nano v3 is distributed silently, updated silently, and runs silently. It is shipped to qualifying Chrome installations among the browser's 3+ billion user base. There is no user-facing toggle to disable it, no documentation of its training data, and, until this research, no public description of its internals.

Le modèle est un moteur de complétion, pas un chatbot. Il n'a aucune notion de "tours" dans une conversation. Nano v3 est distribué silencieusement, mis à jour silencieusement, et s'exécute silencieusement. Il est livré aux installations Chrome éligibles parmi les 3+ milliards d'utilisateurs du navigateur. Il n'existe aucune option visible pour le désactiver, aucune documentation sur ses données d'entraînement, et, jusqu'à cette recherche, aucune description publique de ses mécanismes internes.

SommaireTable of Contents

1 Pas le modèle publiéNot the published model 2 Le mur AndroidThe Android wall 3 Le pivot desktopThe desktop pivot 4 847 tenseurs847 tensors 5 AltUp : le labyrinthe de routageAltUp: the routing labyrinth 6 Chaque octet était correctEvery byte was right 7 Le test canariThe canary test 8 L'interprétation était fausseThe interpretation was wrong 9 Ce qui resteWhat remains 10 Ce que cela signifieWhat this means A Annexes
1

Le modèle IA de Chrome n'est pas celui que Google a publiéChrome's AI model is not the one Google published

Même architecture, modèle différentSame architecture, different model

Google released Gemma 3n E4B as an open model in June 2025. Same architecture as Chrome's Gemini Nano v3: 35 layers, 2048 hidden dimensions, AltUp branching, PLE per-layer embeddings, LAuReL residual connections. Identical blueprint.

Google a publié Gemma 3n E4B comme modèle ouvert en juin 2025. Même architecture que le Gemini Nano v3 de Chrome : 35 couches, 2048 dimensions cachées, branchement AltUp, embeddings PLE par couche, connexions résiduelles LAuReL. Schéma identique.

Different model.

Modèle différent.

The tokenizers are from different families: Gemini (250,100 active tokens) for Chrome, Gemma (262,400 tokens) for E4B. Only 23% Jaccard similarity in token overlap. I compared embeddings for 201 tokens present in both vocabularies. Mean cosine similarity: 0.023. That is indistinguishable from random noise. Vectors from the same model, even after aggressive quantization, show cosim above 0.85.

Les tokenizers appartiennent à des familles différentes : Gemini (250 100 tokens actifs) pour Chrome, Gemma (262 400 tokens) pour E4B. Seulement 23% de similarité de Jaccard en recouvrement de tokens. J'ai comparé les embeddings de 201 tokens présents dans les deux vocabulaires. Similarité cosinus moyenne : 0,023. C'est indiscernable du bruit aléatoire. Des vecteurs issus du même modèle, même après quantification agressive, montrent une cosim supérieure à 0,85.

Security implicationImplication sécuritaire

Chrome's tokenizer contains no <start_of_turn> or <end_of_turn> tokens. No chat formatting at all. This is a completion model designed for directed tasks (summarization, rewriting), not a chatbot. Jailbreaks, prompt injections, or alignment bypasses found on the public Gemma 3n E4B do not transfer to Chrome's on-device model.

Le tokenizer de Chrome ne contient aucun token <start_of_turn> ni <end_of_turn>. Aucun formatage de chat. C'est un modèle de complétion conçu pour des tâches dirigées (résumé, reformulation), pas un chatbot. Les jailbreaks, injections de prompts ou contournements d'alignement trouvés sur le Gemma 3n E4B public ne se transfèrent pas au modèle embarqué de Chrome.

Chrome Gemini Nano v3 Gemma 3n E4B
TokenizerGemini (250,100)Gemma (262,400)
Jaccard overlap23%
Embedding cosim (201 shared tokens)Cosim embedding (201 tokens partagés)0.023
Chat tokensTokens de chatNoneAucun<start_of_turn>, <end_of_turn>
TypeCompletionComplétionChat
2

Le mur AndroidThe Android wall

L'extraction des poids était impossible sur mobileWeight extraction was impossible on mobile

I started on Android. The model ships in Google's Quick Search Box APK, and I decompiled it with jadx (107,277 files in fallback mode). The architecture documentation came from there: weight buffer maps, graph definitions, tensor names and shapes.

J'ai commencé par Android. Le modèle est embarqué dans l'APK Google Quick Search Box, que j'ai décompilé avec jadx (107 277 fichiers en mode fallback). La documentation architecturale vient de là : cartes de buffers de poids, définitions de graphes, noms et formes des tenseurs.

The weights themselves were untouchable.

Les poids eux-mêmes étaient intouchables.

On Android, Qualcomm's NNC compiler tiles every tensor at sub-KB granularity for the Hexagon DSP. Where you'd expect a contiguous 4 MB matrix, you find hundreds of 1-3 KB fragments from different layers interleaved into an opaque blob. No boundaries, no markers, no way to reassemble individual tensors.

Sur Android, le compilateur NNC de Qualcomm découpe chaque tenseur en fragments sub-KB pour le DSP Hexagon. Là où on attendrait une matrice contiguë de 4 Mo, on trouve des centaines de fragments de 1-3 Ko provenant de couches différentes, entrelacés dans un blob opaque. Pas de frontières, pas de marqueurs, aucun moyen de réassembler les tenseurs individuels.

Six approaches, all failedSix approches, toutes échouées

Direct binary scanning, Qualcomm's QNN SDK utilities, Chromium source (closed-source for chromeml), NNC config files (all offsets set to zero), QNN inspection dump (numContextTensors=0), and cross-referencing norms against the public E4B model (zero matches, because different weights).

Scan binaire direct, utilitaires du SDK QNN de Qualcomm, code source Chromium (closed-source pour chromeml), fichiers de configuration NNC (tous les offsets à zéro), dump d'inspection QNN (numContextTensors=0), et recoupement des norms avec le modèle public E4B (zéro correspondance, puisque les poids sont différents).

3

Le pivot desktopThe desktop pivot

Stockage linéaire, pas de DSP HexagonLinear storage, no Hexagon DSP

The same model is distributed to Chrome Desktop via the OptimizationGuide CDN. On desktop, there's no Hexagon DSP. The model runs on CPU via XNNPACK, which is open-source.

Le même modèle est distribué à Chrome Desktop via le CDN OptimizationGuide. Sur desktop, pas de DSP Hexagon. Le modèle tourne sur CPU via XNNPACK, qui est open-source.

I scanned the first layer region at 4 KB granularity. Result: 99.5% of blocks are continuous INT4 data, interrupted only by two F32 scale blocks. No micro-tiling. No fragment interleaving. Linear storage.

J'ai scanné la région de la première couche à une granularité de 4 Ko. Résultat : 99,5% des blocs sont des données INT4 continues, interrompues seulement par deux blocs de scales F32. Pas de micro-tiling. Pas d'entrelacement de fragments. Stockage linéaire.

The go/no-go momentLe moment go/no-go

Positive result. Everything else follows from here.

Résultat positif. Tout le reste en découle.

File layoutDisposition du fichier

The file layout turned out to be straightforward once mapped: 8 MB of encrypted header, a pre-processing region with small mixed tensors, a 256 MB embedding block, 35 layers of FFN and attention data (~56 MB each), a 32 MB block of global AltUp tensors, then 35 tail blocks with per-layer token embeddings (~32 MB each), and padding at the end.

La disposition du fichier s'est avérée lisible une fois cartographiée : 8 Mo d'en-tête chiffré, une région de pré-traitement avec de petits tenseurs mixtes, un bloc d'embedding de 256 Mo, 35 couches de données FFN et attention (~56 Mo chacune), un bloc de 32 Mo de tenseurs AltUp globaux, puis 35 blocs de queue avec les embeddings de tokens par couche (~32 Mo chacun), et du padding à la fin.

Finding the scalesTrouver les scales

The hard part was not finding the weights. INT4 and INT8 quantized data have unmistakable statistical signatures (peaked distributions centered on their zero points). The hard part was finding the dequantization scales.

Le plus dur n'était pas de trouver les poids. Les données quantifiées INT4 et INT8 ont des signatures statistiques reconnaissables (distributions à pic centrées sur leur point zéro). Le plus dur était de trouver les scales de déquantification.

Eight failed attempts before the breakthrough. The scales turned out to be stored as separate F32 blocks adjacent to the weight data, not embedded inside the INT4 blocks as the standard XNNPACK tiled format would suggest. At offset 694 MB, two blocks of 16,384 float32 values each, mean around 0.008, all valid IEEE754. Per-channel FFN scales.

Huit tentatives échouées avant la percée. Les scales étaient stockées comme des blocs F32 séparés adjacents aux données de poids, pas embarquées à l'intérieur des blocs INT4 comme le format tuile standard XNNPACK le laisserait supposer. À l'offset 694 Mo, deux blocs de 16 384 valeurs float32 chacun, moyenne autour de 0,008, toutes en IEEE754 valide. Scales FFN par canal.

First successful dequantization produced a clean Gaussian distribution centered on zero, standard deviation 0.025. The weights were real.

La première déquantification réussie a produit une distribution gaussienne propre centrée sur zéro, écart-type 0,025. Les poids étaient réels.

4

Construire le modèle, un tenseur à la foisBuilding the model, one tensor at a time

847 tenseurs, chacun avec son propre problème847 tensors, each with its own problem

847 tensors. That's what goes into a working Gemini Nano v3: FFN projections (gate, up, down) for 35 layers, attention matrices (Q, K, V, O) for 35 layers, seven types of normalization parameters, AltUp routing coefficients, PLE per-layer embeddings, LAuReL residual projections, three global AltUp tensors, and the main embedding table.

847 tenseurs. C'est ce qu'il faut pour un Gemini Nano v3 fonctionnel : projections FFN (gate, up, down) pour 35 couches, matrices d'attention (Q, K, V, O) pour 35 couches, sept types de paramètres de normalisation, coefficients de routage AltUp, embeddings PLE par couche, projections résiduelles LAuReL, trois tenseurs AltUp globaux, et la table d'embedding principale.

Each tensor type had its own extraction problem.

Chaque type de tenseur avait son propre problème d'extraction.

The alignment bugLe bug d'alignement

Four of 35 layers (L16, L17, L18, L28) failed dequantization. Root cause: floating-point offset arithmetic producing offsets not aligned to 4 bytes. A calculated gap of 16,911,433 bytes is 1 byte off from 4-byte alignment, which shifts all float32 reads and produces systematic garbage. Fix: interpolate from the 31 validated layers, then scan at 4-byte steps in a narrow window.

Quatre couches sur 35 (L16, L17, L18, L28) échouaient à la déquantification. Cause : l'arithmétique d'offset en virgule flottante produisait des offsets non alignés sur 4 octets. Un gap calculé de 16 911 433 octets est décalé d'1 octet par rapport à l'alignement 4 octets, ce qui décale toutes les lectures float32 et produit du garbage systématique. Correction : interpoler depuis les 31 couches validées, puis scanner par pas de 4 octets dans une fenêtre étroite.

The padding directionLa direction du padding

Chrome's XNNPACK runtime appends 3-13 KB of trailing metadata after Q/K/V attention tensors. My initial extraction code skipped bytes at the start of the file instead of the end, shifting all attention data by thousands of bytes and mixing adjacent rows. The GGUF loaded fine, ran at full speed, produced garbage. Cosine similarity against source jumped from 0.02 (random) to 1.000000 once I fixed the skip direction.

Le runtime XNNPACK de Chrome ajoute 3-13 Ko de métadonnées à la fin des tenseurs d'attention Q/K/V. Mon code d'extraction initial sautait les octets au début du fichier au lieu de la fin, décalant toutes les données d'attention de milliers d'octets et mélangeant les lignes adjacentes. Le GGUF se chargeait sans problème, tournait à pleine vitesse, produisait du garbage. La similarité cosinus contre la source est passée de 0,02 (aléatoire) à 1,000000 une fois la direction du skip corrigée.

The L19 trapLe piège L19

Layer 19 is where the attention format transitions from separate Q/K/V/O tensors to a fused QKV block. Unlike layers 20-34 which share K/V weights with earlier layers, L19 has its own K/V projections embedded in the fused block. My extraction treated it as Q-only, leaving K/V as placeholder data filled with Inf values. No load-time warning. Silent corruption that caused multiple regressions across builds.

La couche 19 est celle où le format d'attention passe de tenseurs Q/K/V/O séparés à un bloc QKV fusionné. Contrairement aux couches 20-34 qui partagent les poids K/V avec des couches antérieures, L19 a ses propres projections K/V embarquées dans le bloc fusionné. Mon extraction l'a traitée comme Q uniquement, laissant K/V en données placeholder remplies de valeurs Inf. Aucun avertissement au chargement. Corruption silencieuse qui a causé de multiples régressions entre les builds.

Corruption in the source fileCorruption dans le fichier source

Seven layers contain corrupted values ranging from 1e17 to 1e38 in their norm and scale parameters. Not extraction bugs; the corruption exists in Chrome's weights.bin itself. 25 corrections total, fixed by copying from the nearest clean neighbor layer.

Sept couches contiennent des valeurs corrompues allant de 1e17 à 1e38 dans leurs paramètres de norms et scales. Pas des bugs d'extraction : la corruption existe dans le weights.bin de Chrome lui-même. 25 corrections au total, corrigées en copiant depuis la couche voisine la plus propre.

LAuReL transpositionTransposition LAuReL

Chrome stores these tensors in a different memory layout than what the GGML runtime expects. The writer must transpose the data during requantization, not just swap the declared dimensions.

Chrome stocke ces tenseurs dans un layout mémoire différent de ce qu'attend le runtime GGML. Le writer doit transposer les données pendant la requantification, pas simplement permuter les dimensions déclarées.

5

AltUp : le labyrinthe de routageAltUp: the routing labyrinth

175 tenseurs dispersés dans les gaps inter-couches175 tensors scattered across inter-layer gaps

AltUp splits each token into 4 branches at embedding time. Per-layer predict and correct coefficients route information between branches. Five components per layer, 175 tensors total, scattered across different inter-layer gaps rather than stored contiguously.

AltUp divise chaque token en 4 branches au moment de l'embedding. Des coefficients predict et correct par couche routent l'information entre les branches. Cinq composants par couche, 175 tenseurs au total, dispersés dans différents gaps inter-couches plutôt que stockés de façon contiguë.

The predict coefficients (tiny F16 tensors, 128 bytes each) were found through 256-byte aligned scanning of inter-layer gaps. The correct coefficients required norm-gap scanning. Both relatively clean once located.

Les coefficients predict (minuscules tenseurs F16, 128 octets chacun) ont été trouvés par scan aligné sur 256 octets des gaps inter-couches. Les coefficients correct ont nécessité un scan des gaps de norms. Les deux étaient relativement propres une fois localisés.

The routerLe router

Where the public E4B model stores 4 independent columns in F16 format (a [2048, 4] matrix), Chrome stores a single [2048, 1] vector in INT16 little-endian, sign-extended to INT32, divided by 2^15. This vector is broadcast identically across all 4 branches. All modalities receive the same routing signal. I tested and eliminated 12+ alternative hypotheses before landing on this format.

Là où le modèle public E4B stocke 4 colonnes indépendantes en format F16 (une matrice [2048, 4]), Chrome stocke un unique vecteur [2048, 1] en INT16 little-endian, étendu en signe vers INT32, divisé par 2^15. Ce vecteur est diffusé de façon identique sur les 4 branches. Toutes les modalités reçoivent le même signal de routage. J'ai testé et éliminé 12+ hypothèses alternatives avant d'atterrir sur ce format.

The global AltUp tensors live in a 32 MB block after the last layer. All three verified at cosim 0.999987 or above against Chrome source data.

Les tenseurs AltUp globaux résident dans un bloc de 32 Mo après la dernière couche. Les trois sont vérifiés à cosim 0,999987 ou plus contre les données source de Chrome.

6

Chaque octet était correctEvery byte was right

845/847 tenseurs vérifiés, charabia multilingue en sortie845/847 tensors verified, multilingual garbage output

At that point, 845 of 847 tensors were verified at cosine similarity 0.9999+ against Chrome source data. The two unverified tensors (L34 LAuReL) had no extractable source to compare against. The public E4B model ran correctly in the same llama.cpp build, proving the runtime implementation works.

À ce stade, 845 des 847 tenseurs étaient vérifiés à une similarité cosinus de 0,9999+ contre les données source de Chrome. Les deux tenseurs non vérifiés (L34 LAuReL) n'avaient aucune source extractible pour comparaison. Le modèle public E4B tournait correctement dans le même build llama.cpp, prouvant que l'implémentation runtime fonctionne.

The GGUF loaded without warnings. Inference ran at full speed. Every tensor matched its source.

Le GGUF se chargeait sans avertissements. L'inférence tournait à pleine vitesse. Chaque tenseur correspondait à sa source.

The output was stable multilingual garbage. Russian, Japanese, and Thai characters mixed together. No NaN, no crashes, input-sensitive (different prompts produced different garbage), generating indefinitely.

La sortie était du charabia multilingue stable. Du russe, du japonais et du thaï mélangés. Pas de NaN, pas de crash, sensible à l'entrée (des prompts différents produisaient du charabia différent), générant indéfiniment.

PerplexityPerplexité

4.6 billion. The E4B reference model scores 10.25.4,6 milliards. Le modèle de référence E4B obtient 10,25.

7

Le test canariThe canary test

Éliminer des classes entières de suspectsEliminating entire classes of suspects

Replacing F32 tensors with E4B valuesRemplacement des tenseurs F32 par les valeurs E4B

To isolate the problem, I replaced all 422 F32 tensors in the Chrome GGUF (norms, AltUp coefficients, scales) with values from the public E4B model. If the F32 tensors were the problem, E4B values should fix it.

Pour isoler le problème, j'ai remplacé les 422 tenseurs F32 du GGUF Chrome (norms, coefficients AltUp, scales) par les valeurs du modèle public E4B. Si les tenseurs F32 étaient le problème, les valeurs E4B devaient le résoudre.

Still garbage.

Toujours du charabia.

This eliminated an entire class of suspects. The norms were not the cause. The AltUp coefficients were not the cause. The scales were not the cause.

Cela éliminait une classe entière de suspects. Les norms n'étaient pas en cause. Les coefficients AltUp n'étaient pas en cause. Les scales n'étaient pas en cause.

Converting Q4_0 to Q8_0Conversion Q4_0 vers Q8_0

Next: I converted all Q4_0 tensors to Q8_0, producing a 6.7 GB GGUF. If the 4-bit quantization format was losing information, 8-bit should recover it.

Étape suivante : j'ai converti tous les tenseurs Q4_0 en Q8_0, produisant un GGUF de 6,7 Go. Si le format de quantification 4 bits perdait de l'information, le 8 bits devait la récupérer.

Still garbage.

Toujours du charabia.

This eliminated quantization precision as a cause. The problem was not information loss from aggressive quantization. The problem was something else entirely.

Cela éliminait la précision de quantification comme cause. Le problème n'était pas une perte d'information due à une quantification agressive. Le problème était tout autre.

8

Les octets étaient corrects. L'interprétation était fausse.The bytes were correct. The interpretation was wrong.

INT4 signé vs unsigned offset-8Signed INT4 vs unsigned offset-8

Chrome stores FFN weights as signed INT4 two's complement. In this encoding, nibble value 0 means zero, values 1-7 mean +1 to +7, and values 8-15 mean -8 to -1. The nibble value 0 is the most frequent (about 16.5% of all values, representing zero-weight connections). Nibble value 8 literally never appears (0.0% frequency), because -8 is the extreme negative value and essentially unused.

Chrome stocke les poids FFN en INT4 signé en complément à deux. Dans cet encodage, la valeur de nibble 0 signifie zéro, les valeurs 1-7 signifient +1 à +7, et les valeurs 8-15 signifient -8 à -1. La valeur de nibble 0 est la plus fréquente (environ 16,5% de toutes les valeurs, représentant les connexions à poids nul). La valeur de nibble 8 n'apparaît littéralement jamais (0,0% de fréquence), car -8 est la valeur négative extrême et essentiellement inutilisée.

The GGUF Q4_0 format interprets the same nibble values as unsigned offset-8. In Q4_0, nibble 0 means -8, nibble 8 means 0, and nibble 15 means +7.

Le format GGUF Q4_0 interprète les mêmes valeurs de nibble comme unsigned offset-8. En Q4_0, nibble 0 signifie -8, nibble 8 signifie 0, et nibble 15 signifie +7.

Same bytes. Different meaning.Mêmes octets. Sens différent.

Without correction, every nibble that Chrome means as "zero" (value 0), GGML interprets as "-8." Every nibble that Chrome means as "+7" (value 7), GGML interprets as "-1." A systematic DC offset on every single weight, accumulated through 16,384-wide matrix multiplications across 35 layers.

Sans correction, chaque nibble que Chrome interprète comme "zéro" (valeur 0), GGML l'interprète comme "-8". Chaque nibble que Chrome interprète comme "+7" (valeur 7), GGML l'interprète comme "-1". Un offset DC systématique sur chaque poids, accumulé à travers des multiplications matricielles de largeur 16 384 sur 35 couches.

Nibble valueValeur nibble Chrome (signed TC)Chrome (signé TC) GGUF Q4_0 (unsigned-8)
00 (~16.5%)-8
7+7-1
8-8 (0.0%)0
15-1+7

The fix: XOR 0x8Le correctif : XOR 0x8

The fix is a per-nibble XOR with 0x8. Flip the high bit of each 4-bit value. This converts between signed two's complement and unsigned offset-8 representation.

Le correctif est un XOR par nibble avec 0x8. Inverser le bit haut de chaque valeur 4 bits. Cela convertit entre la représentation en complément à deux signé et la représentation unsigned offset-8.

Result: perplexity dropped from 4,590,744,429 to 289,387. A 15,870x improvement from fixing the interpretation of bytes that were already correct.

Résultat : la perplexité est passée de 4 590 744 429 à 289 387. Une amélioration de 15 870x en corrigeant l'interprétation d'octets qui étaient déjà corrects.

The verification paradoxLe paradoxe de vérification

Per-tensor cosine similarity against Chrome source data was 1.000000 for all 105 FFN tensors, before and after the fix. Because the verification code dequantized using the same signed two's complement formula as Chrome. The bytes matched their source. The problem was that GGML's Q4_0 dequantization uses a different formula. Cosine similarity verification is necessary but not sufficient. You must also verify that the dequantization convention matches the target runtime.

La similarité cosinus par tenseur contre les données source de Chrome était de 1,000000 pour les 105 tenseurs FFN, avant et après le correctif. Parce que le code de vérification déquantifiait avec la même formule en complément à deux signé que Chrome. Les octets correspondaient à leur source. Le problème était que la déquantification Q4_0 de GGML utilise une formule différente. La vérification par similarité cosinus est nécessaire mais pas suffisante. Il faut aussi vérifier que la convention de déquantification correspond au runtime cible.

Not all INT4 tensors needed the fixTous les tenseurs INT4 ne nécessitaient pas le correctif

Only the FFN writer copied nibbles directly without conversion. The per-layer embedding writer already handled the signed-to-unsigned mapping internally, meaning the XOR8 fix was specific to the 105 FFN tensors. Applying it to PLE data caused a double conversion and made results worse (PPL 6.5 billion).

Seul le writer FFN copiait les nibbles directement sans conversion. Le writer des embeddings par couche gérait déjà le mapping signé vers unsigned en interne, ce qui signifie que le correctif XOR8 était spécifique aux 105 tenseurs FFN. L'appliquer aux données PLE provoquait une double conversion et aggravait les résultats (PPL 6,5 milliards).

9

Ce qui resteWhat remains

PPL 289 387 n'est pas PPL 10,25PPL 289,387 is not PPL 10.25

PPL 289,387 is not PPL 10.25. The 28,000x remaining gap is real, and I have not closed it.

PPL 289 387 n'est pas PPL 10,25. L'écart restant de 28 000x est réel, et je ne l'ai pas comblé.

Primary suspect: missing bias termsSuspect principal : termes de biais manquants

I found INT32 values in the gaps between Q and K weight blocks, 2,048 values per layer, consistent with per-output-channel biases. String analysis of Chrome's optimization_guide_internal.dll confirms bias support. These biases affect every matrix multiplication and are not represented in the GGUF format I'm writing.

J'ai trouvé des valeurs INT32 dans les gaps entre les blocs de poids Q et K, 2 048 valeurs par couche, cohérentes avec des biais par canal de sortie. L'analyse des chaînes de caractères de optimization_guide_internal.dll de Chrome confirme le support des biais. Ces biais affectent chaque multiplication matricielle et ne sont pas représentés dans le format GGUF que j'écris.

Secondary suspect: unknown weight transformsSecond suspect : transformations de poids inconnues

The DLL contains an obfuscation module (obfuscation.cc, odml.infra.proto.ObfuscationParams), internal naming conventions that reveal the full tensor hierarchy, LoRA support for rank 16/32, FIRE positional encoding parameters, and support for multiple model families (Gemma3N, Gemma3, Qwen3, TinyGemma). There may be reshaping or permutation steps that the closed-source runtime applies before inference.

La DLL contient un module d'obfuscation (obfuscation.cc, odml.infra.proto.ObfuscationParams), des conventions de nommage internes qui révèlent la hiérarchie complète des tenseurs, le support LoRA pour les rangs 16/32, des paramètres d'encodage positionnel FIRE, et le support de multiples familles de modèles (Gemma3N, Gemma3, Qwen3, TinyGemma). Il peut y avoir des étapes de reshape ou de permutation que le runtime closed-source applique avant l'inférence.

Minor suspectsSuspects mineurs

L19 K/V using L18's values as proxy (one layer with approximate weights), two norm values using fallback constants, two AltUp coefficients found as all-zeros in the source data. A norm off-by-one shift accounted for some error: the inter-layer gap between layer N-1 and layer N contains 7 F32 norm blocks, 4 belonging to the previous layer and 3 to the next. My initial extraction assigned all 7 to the same layer. Fixing this brought 3 of 7 norm types from cosim 0.85 to 0.999+.

K/V de L19 utilisant les valeurs de L18 comme proxy (une couche avec des poids approximatifs), deux valeurs de norms utilisant des constantes de repli, deux coefficients AltUp trouvés à zéro dans les données source. Un décalage off-by-one des norms expliquait une part de l'erreur : le gap inter-couches entre la couche N-1 et la couche N contient 7 blocs de norms F32, 4 appartenant à la couche précédente et 3 à la suivante. Mon extraction initiale les attribuait tous les 7 à la même couche. La correction a fait passer 3 des 7 types de norms de cosim 0,85 à 0,999+.

10

Ce que cela signifieWhat this means

Weight protection across platforms does not workLa protection des poids multi-plateforme ne fonctionne pas

Qualcomm's NNC tiling is an effective barrier on Android. The same model shipped via Chrome Desktop stores weights linearly. Multi-platform distribution means protection is only as strong as the weakest runtime.

Le tiling NNC de Qualcomm est une barrière efficace sur Android. Le même modèle distribué via Chrome Desktop stocke les poids de façon linéaire. La distribution multi-plateforme signifie que la protection n'est aussi solide que le runtime le plus faible.

Chrome ships a proprietary model with no public documentationChrome embarque un modèle propriétaire sans documentation publique

The training data, alignment procedures, and safety testing for Chrome's on-device AI are unknown. The public Gemma 3n model cannot serve as a proxy for understanding what Chrome's AI does.

Les données d'entraînement, les procédures d'alignement et les tests de sécurité de l'IA embarquée de Chrome sont inconnus. Le modèle public Gemma 3n ne peut pas servir de proxy pour comprendre ce que fait l'IA de Chrome.

Extracting weights is not enough to reproduce a modelExtraire les poids ne suffit pas à reproduire un modèle

The gap between "correct bytes" and "working inference" has layers. Format interpretation (signed vs unsigned INT4) caused a 15,870x perplexity gap despite byte-level verification showing a match. Missing components (biases, runtime transforms) create further distance. Non-standard architecture elements (AltUp, PLE, LAuReL) interact in ways that only the original runtime fully implements.

L'écart entre "octets corrects" et "inférence fonctionnelle" comporte plusieurs niveaux. L'interprétation du format (INT4 signé vs unsigned) a causé un écart de perplexité de 15 870x malgré une vérification au niveau des octets montrant une correspondance. Les composants manquants (biais, transformations runtime) creusent encore la distance. Les éléments architecturaux non standard (AltUp, PLE, LAuReL) interagissent d'une façon que seul le runtime original implémente complètement.

The obfuscation tradeoffLe compromis d'obfuscation

The header and footer of weights.bin are encrypted (entropy 7.95/8.0 bits), protecting the tensor map, offsets, and metadata. The bulk weight data (10 MB to end-20 KB) is stored as raw quantized integers at 5-7 bits entropy. Encrypting 4 GB of weights would add CPU overhead on every inference call. Encrypting only the metadata prevents trivial documentation without impacting performance.

L'en-tête et le pied de page de weights.bin sont chiffrés (entropie 7,95/8,0 bits), protégeant la carte des tenseurs, les offsets et les métadonnées. Le gros des données de poids (10 Mo à fin-20 Ko) est stocké comme des entiers quantifiés bruts à 5-7 bits d'entropie. Chiffrer 4 Go de poids ajouterait une surcharge CPU à chaque appel d'inférence. Chiffrer uniquement les métadonnées empêche la documentation triviale sans impacter les performances.

Prior workTravaux antérieurs

To date, no other public effort has successfully extracted and run Chrome Gemini Nano v3. Previous work by dejanseo covered v1 and v2 only. QuietImpostor attempted a GGUF conversion of v1 but reported it non-functional. antimatter15 documented the Gemma 3n architecture in detail but explicitly noted the Chrome model is "far from runnable." The architecture is documented by community research. The weights are extractable. But making them produce correct output in a different runtime remains an open problem.

À ce jour, aucun autre effort public n'a réussi à extraire et faire tourner le Gemini Nano v3 de Chrome. Les travaux antérieurs de dejanseo couvraient les v1 et v2 uniquement. QuietImpostor a tenté une conversion GGUF de la v1 mais l'a signalée comme non fonctionnelle. antimatter15 a documenté l'architecture Gemma 3n en détail mais a explicitement noté que le modèle Chrome est "far from runnable". L'architecture est documentée par la recherche communautaire. Les poids sont extractibles. Mais les faire produire une sortie correcte dans un runtime différent reste un problème ouvert.

Responsible disclosureDivulgation responsable

This research was conducted on publicly distributed model files via Chrome's OptimizationGuide CDN. No authentication bypass, exploitation, or unauthorized access was performed. The extracted GGUF file is not published, as the weights remain Google's proprietary intellectual property.

Cette recherche a été menée sur des fichiers de modèle distribués publiquement via le CDN OptimizationGuide de Chrome. Aucun contournement d'authentification, exploitation ou accès non autorisé n'a été effectué. Le fichier GGUF extrait n'est pas publié, les poids restant la propriété intellectuelle de Google.

A

Annexes

A. Complete architectureArchitecture complète

Gemini Nano v3 (nano2)
Type: Decoder-only Transformer (completion model, no chat tokens)
Vocab: 256,128 tokens (250,100 active + padding)
Hidden dim: 2,048
Head dim: 256 (8 query heads, 2 KV heads, GQA 4:1)
Intermediate (FFN): 16,384
Layers: 35 = [4 local + 1 global] x 7 groups
  Local layers: sliding_window = 512, RoPE theta = 10,000
  Global layers: full attention, RoPE theta = 1,000,000
Quantization: a16w8w4 (LPBQ)
  Activations: INT16
  Attention weights (Q,K,V,O): INT8, unsigned, zero_point=128
  FFN weights (gate,up,down): INT4, signed two's complement
Embedding: INT3 (separate file on Android, 772 bytes/token)
LoRA: rank 16/32 depending on layer (applied at runtime)
AltUp: 4 branches with predict/correct coefficients
PLE: Per-Layer Embeddings (35 layers x 256 dims)
LAuReL: Low-Rank Residual (rank 64)
Activation sparsity: layers 0-9 (Gaussian TopK, scale 1.6449)

Total text model: 2.96 GB
Speculative drafter: 4 layers, hidden=512, 114 MB
Vision encoder: 40 layers, input 512x512, 762 MB + 142 MB Hexagon DSP
Total: 4.27 GB

B. Three layer typesTrois types de couches

TypeLayersCouchesSize/layerTaille/coucheCharacteristicCaractéristique
A (standard local)0-1964.1 MBStandard INT4 FFN + INT8 attentionINT4 FFN standard + INT8 attention
B (heavy local)20-2997.9 MBFFN gate+up promoted to INT8 (+33.8 MB)FFN gate+up promus en INT8 (+33,8 Mo)
C (shared-KV local)30-3462.0 MBKV projections shared across layers (-2.1 MB)Projections KV partagées entre couches (-2,1 Mo)

C. Two attention architecturesDeux architectures d'attention

LayersCouchesLayoutAttention sizeTaille attention
L0-L18Q, K, V, O separateQ, K, V, O séparés~6.03 MB
L19-L34QKV fused + O separateQKV fusionné + O séparé~4.01 MB

For L19-34, the fused QKV tensor [2048 x 2048] contains Q(1024) + K(512) + V(512) concatenated along the output dimension.Pour L19-34, le tenseur QKV fusionné [2048 x 2048] contient Q(1024) + K(512) + V(512) concaténés sur la dimension de sortie.

D. LPBQ quantization detailsDétails de quantification LPBQ

INT4 (FFN + PLE + AltUp)
Signed two's complement:
  nibble 0 = value 0, nibble 7 = +7, nibble 8 = -8, nibble 15 = -1
  Nibble 0 is the most frequent (~16.5% of values) = zero weight
  Nibble 8 never occurs (0.0%)
  Distribution: Gaussian centered on 0
  Per-row F32 dequantization scales
INT8 (Attention Q,K,V,O)
Unsigned, zero_point = 128
  Byte value 128 = zero weight (~3%)
  Distribution: symmetric around 128
  Per-row F32 dequantization scales

E. File layout (weights.bin)Disposition du fichier (weights.bin)

weights.bin layout
Offset (MB)    Content
0-8            Header (high entropy, encrypted/obfuscated)
8-10           Pre-process (small F32/INT4/INT8 tensors)
10-396         Alternating small mixed blocks
396-652        Embedding INT4 (262,144 x 1024 bytes/row)
653-654        Embedding scales (262,144 F32, mean=0.0077)
694            Two pre-L0 norms (mean=1.67 and mean=6.96)
728-2755       35 layers (FFN INT4, norms F32, Attention INT8)
               ~56 MB/layer (main body)
2755-2787      AltUp globals (32 MB)
2787-3910      35 tail blocks x ~32 MB each
               per_layer_token_embd [262144, 256] INT4
3910-4072      Post-tail: sparse data + zeros + padding

F. Tensor extraction inventory (847 tensors)Inventaire d'extraction des tenseurs (847 tenseurs)

ComponentComposantTensorsTenseursFormatVerificationVérification
FFN (gate/up/down) x 35105Q4_0cosim 1.000000 (XOR8)
Attention Q/K/V/O x 35140Q8_0cosim 1.000000
PLE (gate + proj + norm)105Q8_0/F32cosim 0.9999+
LAuReL (l + r + norm)105Q8_0/F32cosim 0.9999+
AltUp (5 per layer)175F16/F32cosim 1.0
AltUp globals3Q8_0cosim 0.999987+
Base norms x 35140F32Exact matchCorrespondance exacte
QK norms x 3570F32E4B proxy
per_layer_token_embd1Q4_0cosim 0.997-0.999
Embedding (token_embd)1Q4_0cosim 0.9999+
Total847845/847

G. 55+ hypotheses eliminated (selection)55+ hypothèses éliminées (sélection)

#HypothesisHypothèseHow eliminatedComment éliminée
1NNC tiling on desktopTiling NNC sur desktop4 KB scan: 99.5% continuous INT4Scan 4 Ko : 99,5% INT4 continu
4writeQ8_0 skip directionDirection du skip writeQ8_0cosim jumped 0.02 to 1.000000cosim passée de 0,02 à 1,000000
5llama.cpp runtime bugBug runtime llama.cppE4B works in same buildE4B fonctionne dans le même build
6F32 norms/scales wrongNorms/scales F32 erronésCanary: E4B norms still garbageTest canari : norms E4B = charabia
7Q4_0 quantization formatFormat de quantification Q4_0All-Q8_0 GGUF: same garbageGGUF tout-Q8_0 : même charabia
11gate/up swapInversion gate/upPPL 5.5B = worsePPL 5,5 milliards = pire
14Obfuscation in weights.binObfuscation dans weights.binHeader/footer encrypted, bulk in clearEn-tête/pied chiffrés, masse en clair
15Norm off-by-one shiftDécalage off-by-one des norms3 norms fixed, cosim 0.85 to 0.999+3 norms corrigées, cosim 0,85 à 0,999+
16PLE INT4 needs XOR8PLE INT4 nécessite XOR8PPL 6.5B = worse (double conversion)PPL 6,5 milliards = pire (double conversion)

H. INT3 embedding format (Android)Format d'embedding INT3 (Android)

sm8650_embedding_table.bin
197,730,816 bytes
256,128 tokens x 772 bytes/token (exactly)
Per token: 768 bytes = 2,048 INT3 values (LSB-first bitpacking)
           + 4 bytes continuation data
No header, no footer, no per-token metadata
Distribution: symmetric around 3.5 (zero point likely at 4)
Dequantization scale: not stored, likely hardcoded in Hexagon DSP binary

I. Model filesFichiers du modèle

Chrome Desktop model version: 2025.8.8.1141

FileFichierSizeTailleContentContenu
weight_shared_serialized_20251031.bin3.9 GBAll model weights (text + drafter + vision)Tous les poids (texte + drafter + vision)
sm8650_embedding_table.bin197.7 MBVocabulary embedding table (INT3)Table d'embedding vocabulaire (INT3)
text_model_conf.json9,392 linesNNC compiler configurationConfiguration compilateur NNC
nano2_model_inspection.json225,961 linesQNN runtime inspectionInspection runtime QNN
sentencepiece.model7.1 MBTokenizer (250,100 active tokens)
on_device_model_execution_config.pb138 BValidation promptsPrompts de validation

J. Extraction toolsOutils d'extraction

ToolOutilPurposeFonction
desktop_header_map.phpHeader cartography (0-20 MB)Cartographie de l'en-tête (0-20 Mo)
desktop_find_scales.phpF32 scale block discoveryDécouverte des blocs de scales F32
desktop_dequant_final.phpValidated dequantizationDéquantification validée
desktop_extract_full.phpFull tensor extraction (690+ files)Extraction complète des tenseurs (690+ fichiers)
desktop_write_gguf_v5.phpGGUF writer (847 tensors, 1200+ lines)Writer GGUF (847 tenseurs, 1200+ lignes)
patch_laurel_transpose.phpLAuReL transposition fixCorrectif transposition LAuReL
fix_corrupt_sources.php25 corruption fixes across 7 layers25 corrections de corruption sur 7 couches
compare_embeddings.phpChrome vs E4B embedding comparisonComparaison embeddings Chrome vs E4B
strings_dll.phpChromeML DLL string extractionExtraction de chaînes de la DLL ChromeML

K. Statistical signatures for model binary analysisSignatures statistiques pour l'analyse de binaires de modèles

Data typeType de donnéesSignature
INT4 signed TCMost frequent nibble = 0, nibble 8 never appears. Byte 0x00 peaks.Nibble le plus fréquent = 0, nibble 8 n'apparaît jamais. Octet 0x00 domine.
INT8 zp=128Most frequent byte = 0x80, symmetric distribution.Octet le plus fréquent = 0x80, distribution symétrique.
F32 scalesSmall positive values (mean 0.008-0.033), 100% valid IEEE754.Petites valeurs positives (moyenne 0,008-0,033), 100% IEEE754 valide.
F32 normsMean 1.0-7.0, signed values, 100% valid IEEE754.Moyenne 1,0-7,0, valeurs signées, 100% IEEE754 valide.
Padding0x00 repeated or high entropy.0x00 répétés ou haute entropie.

Format detection is not conversionDétecter le format n'est pas le convertir

Detecting the storage format does not mean a conversion is needed. Check what the GGUF writer does with the data before applying any transform; the writer may already handle the conversion internally (as was the case for PLE tensors in this project).

Détecter le format de stockage ne signifie pas qu'une conversion est nécessaire. Vérifiez ce que fait le writer GGUF avec les données avant d'appliquer une transformation ; le writer peut gérer la conversion en interne (comme c'était le cas pour les tenseurs PLE dans ce projet).

L. Project statisticsStatistiques du projet

MetricMétriqueValueValeur
Scripts createdScripts créés250+ PHP + 15 Python + 8 PowerShell
Desktop model sizeTaille du modèle desktop4,269,932,544 bytes
GGUF producedGGUF produit4,061,596,736 bytes (847 tensors)
GGUF iterationsItérations GGUFv1 (387) → v14 (847 tensors)
Tensor verificationVérification des tenseurs845/847 cosim 0.9999+
Perplexity achievedPerplexité atteinte289,387 (vs E4B: 10.25)
Hypotheses testedHypothèses testées55+
Bugs found in own codeBugs trouvés dans le code10 major
Google codenames decodedNoms de code Google décodés52

Autres sources et étudesOther sources and studies

RESONEO