← Blog

Anatomie d'un modèle : ce que contient un fichier de poids

Quand on déploie un LLM open-source, on télécharge « un modèle ». Concrètement, on récupère un dossier de fichiers. Pas un binaire, pas un service, pas une base de connaissances : un dossier. Comprendre ce qu'il y a dedans, et surtout ce qu'il n'y a pas, change la façon dont on raisonne sur l'inférence, la mémoire et le sizing.

Cet article dissèque le modèle qui tourne en production sur le serveur d'inférence de Sover.tech : Gemma 4 12B, identifiant gemma-4-12b-it. Tous les chiffres cités sont ceux de ce modèle précis, lus dans ses fichiers et dans son config.json, pas des ordres de grandeur inventés.

Un modèle, c'est un dossier de fichiers

Voici le contenu réel du dossier chargé par le pod vLLM. Sept fichiers, tous copiables, zippables, inertes.

Fichier Taille Rôle
model.safetensors9,6 GoLes poids. Tout le reste ne fait que décrire comment les utiliser.
config.json5,8 KoL'architecture : la forme de la fonction, sans aucun poids.
tokenizer.json31 MoLe vocabulaire : la liste des tokens connus.
chat_template.jinja18 KoLe formatage des tours de parole.
tokenizer_config.json2,7 KoParamètres du tokenizer.
generation_config.json255 oDéfauts d'échantillonnage (temperature, top_k, top_p).
processor_config.json1,4 KoPrétraitement (Gemma 4 est multimodal).

Un seul fichier pèse : model.safetensors, à 9,6 Go. C'est là que sont les poids. Le reste tient dans quelques mégaoctets et sert uniquement à savoir comment lire et alimenter ces poids.

Les poids : des nombres, et rien d'autre

Un poids (ou paramètre, les deux mots sont interchangeables) est un nombre. Un seul. Personne n'en choisit la valeur : pendant l'entraînement, on présente des milliards de phrases au modèle, il se trompe, et chaque poids est ajusté d'un cheveu dans la direction qui réduit l'erreur. On recommence, pendant des mois. Les poids sont le résidu de ce processus.

Le « 12B » de gemma-4-12b-it, c'est ça, et rien d'autre : environ douze milliards de ces nombres. Aucun d'eux ne signifie quoi que ce soit isolément. Le nom du modèle est littéralement le décompte de ses poids.

Point important pour la suite : le fichier ne contient aucun code, aucune règle écrite, aucune copie des textes d'entraînement. Pas de « si l'utilisateur demande X, répondre Y ». Que des nombres. Attention au raccourci, cela dit : « pas de texte stocké » ne veut pas dire « pas de connaissance ». Ces poids encodent une masse d'information statistique apprise. Ils ne stockent pas les textes d'entraînement, ils stockent les paramètres d'une fonction qui a appris leurs régularités.

Le format safetensors et les tenseurs

Les poids sont rangés dans un format nommé safetensors, où ils sont groupés en tenseurs. Un tenseur, c'est un bloc de nombres avec une forme : une liste, un tableau, un cube. Il y a 1662 tenseurs dans ce fichier, et leurs noms disent exactement où chaque paquet de poids se branche dans l'architecture.

model.language_model.embed_tokens.weight            [262144, 3840]
model.language_model.layers.0.self_attn.q_proj.weight
model.language_model.layers.0.mlp.gate_proj.weight
model.language_model.layers.0.input_layernorm.weight [3840]
...  x 48 couches

On lit la structure directement dans les noms : une table d'embeddings à l'entrée, puis 48 couches numérotées de 0 à 47, chacune avec ses projections d'attention (q_proj, k_proj, v_proj) et son bloc feed-forward (gate_proj, up_proj, down_proj). Le safetensors n'est pas une boîte noire opaque : c'est un dictionnaire de tenseurs nommés, dont l'en-tête se lit sans charger les poids.

L'architecture tient dans config.json

Si les poids sont le contenu, config.json est le plan. Il décrit la forme de la fonction sans un seul poids dedans. Les lignes qui comptent pour Gemma 4 12B :

{
  "text_config": {
    "num_hidden_layers": 48,
    "hidden_size": 3840,
    "num_attention_heads": 16,
    "num_key_value_heads": 8,
    "head_dim": 256,
    "intermediate_size": 15360,
    "vocab_size": 262144,
    "max_position_embeddings": 262144
  }
}
  • num_hidden_layers: 48 : quarante-huit couches empilées.
  • hidden_size: 3840 : la largeur des vecteurs qui circulent d'une couche à l'autre.
  • num_attention_heads: 16 et num_key_value_heads: 8 : de la grouped-query attention, deux têtes de requête partagent une même paire clé/valeur. Ce détail réduit la taille du KV cache, on y reviendra dans un autre article.
  • vocab_size: 262144 : le nombre de tokens que le modèle connaît.
  • max_position_embeddings: 262144 : la longueur de contexte maximale.

Cinq lignes utiles suffisent à décrire l'ossature d'un modèle de douze milliards de paramètres. Changer l'un de ces chiffres, c'est changer de modèle. Changer les poids sans toucher à ces chiffres, c'est réentraîner le même modèle.

Du texte aux nombres : token et embedding

Reste à savoir comment du texte entre dans cette fonction faite de nombres. Deux étapes.

D'abord le token : un bout de mot associé à un numéro. Le modèle ne connaît que les 262 144 tokens listés dans tokenizer.json. Le mot « bonjour », par exemple, porte le numéro 117 874.

Ensuite l'embedding : chaque token possède un vecteur de 3840 nombres (la valeur de hidden_size), lu directement dans le tenseur embed_tokens.weight. C'est le point d'entrée, l'endroit précis où le texte devient des nombres manipulables. Voici le vrai vecteur de « bonjour », extrait du modèle :

bonjour  ->  token 117874  ->  embed_tokens.weight[117874]

 0.02759  -0.02759   0.00054   0.02441  -0.02185   0.00433   0.00723  -0.00531
-0.02844   0.01257  -0.00192   0.00620  -0.01587  -0.00066   0.00317   0.00595
-0.01184   0.00842   0.01721   0.02075   0.00922   0.00800  -0.02600  -0.00909
...  et ainsi de suite jusqu'au 3840e nombre

Ces 3840 nombres sont le mot à l'entrée du modèle. Attention au contresens fréquent : ce ne sont pas 3840 sens ou 3840 contextes du mot, ce sont les 3840 coordonnées d'un seul point dans un espace à 3840 axes. Et c'est seulement le point de départ : dès la première couche, cette représentation est retravaillée, puis change encore à chaque couche suivante. Les valeurs affichées ici ne sont donc pas la représentation « finale » de « bonjour », seulement son point d'entrée. Ce qu'il devient en traversant les couches est le sujet du prochain article.

Ce que ça implique

Trois conséquences pratiques découlent directement de cette anatomie.

Un modèle est inerte. Le dossier ne « fait » rien. C'est le moteur d'inférence (vLLM, Ollama) qui charge les poids en mémoire et exécute la fonction. À température nulle et à implémentations proches, le même fichier de poids donne des sorties très proches d'un moteur à l'autre. Mais « très proches » n'est pas « identiques » : de petites différences numériques (FlashAttention, quantification, ordre des opérations, précision FP16/BF16) suffisent à faire diverger la génération au bout de quelques dizaines de tokens.

Changer de modèle = changer le fichier. L'essentiel du comportement, la connaissance apprise, tient dans model.safetensors : remplacer le modèle revient à échanger ce fichier et à relire son config.json. Le matériel en dessous ne s'en aperçoit pas. Les poids ne font pas tout pour autant : le rendu final dépend aussi du tokenizer, du template de chat, des paramètres de génération (temperature, top_p) et du moteur d'inférence, ces quelques fichiers légers qui accompagnent les poids.

La taille du fichier fixe le plancher de mémoire. 9,6 Go de poids, c'est 9,6 Go qu'il faut loger en VRAM avant de pouvoir répondre à la moindre requête. Mais c'est un plancher, pas le total : s'ajoutent le KV cache, les activations, les buffers temporaires et l'overhead du runtime. Sur un contexte long, le KV cache peut même dépasser le poids du modèle. Le fichier donne le premier chiffre d'un dimensionnement, le reste se calcule.

Dans le prochain article, on regarde ce que veulent dire les « 3840 dimensions » et les « 48 couches », et ce qui se passe réellement à l'intérieur d'une couche quand un mot la traverse.