DEV Community

Cover image for Comment Créer un LLM de Zéro : Guide et Enseignements
Antoine Laurent
Antoine Laurent

Posted on • Originally published at apidog.com

Comment Créer un LLM de Zéro : Guide et Enseignements

En bref

Construire un modèle de langage minimal à partir de zéro prend moins de 300 lignes de Python. Le processus révèle exactement comment fonctionnent la tokenisation, l'attention et l'inférence, ce qui fait de vous un bien meilleur consommateur d'API lorsque vous intégrez des LLM de production dans vos applications.

Essayez Apidog dès aujourd'hui

Introduction

La plupart des développeurs traitent les modèles de langage comme des boîtes noires. Vous envoyez du texte, des jetons en sortent, et quelque part entre les deux, la magie opère. Ce modèle mental fonctionne bien jusqu'à ce que vous ayez besoin de déboguer une intégration d'API cassée, d'ajuster les paramètres d'échantillonnage, ou de comprendre pourquoi votre modèle continue d'halluciner des données structurées.

GuppyLM, un projet qui a récemment atteint la première page de HackerNews avec 842 points, rend les mécanismes internes visibles. C'est un transformeur de 8,7 millions de paramètres écrit à partir de zéro en Python. Il s'entraîne en moins d'une heure sur un GPU grand public. Le code tient dans un seul fichier. L'objectif n'est pas de concurrencer GPT-4 ; c'est de démystifier ce que font réellement les LLM.

Cet article explique comment construire un petit LLM, ce que fait chaque composant et ce que la compréhension de ces mécanismes internes vous apprend lorsque vous travaillez professionnellement avec des API d'IA.

💡
Si vous testez des intégrations d'API d'IA, les Scénarios de Test d'Apidog vous permettent de vérifier les réponses en streaming, d'affirmer la structure des jetons et de simuler des complétions de cas limites sans gaspiller vos crédits de production. Plus à ce sujet plus tard.

Qu'est-ce qui rend un modèle de langage "minuscule" ?

Un LLM de production comme GPT-4 possède des centaines de milliards de paramètres. Un LLM "minuscule" se situe dans la plage de 1M à 25M de paramètres. Des projets comme GuppyLM (8,7M), nanoGPT de Karpathy (124M) et MicroLM (1-2M) entrent tous dans cette catégorie.

Les LLM minuscules permettent de :

  • S'entraîner sur un ordinateur portable ou Google Colab
  • Tenir entièrement en mémoire CPU
  • Être inspectés, modifiés et débogués au niveau des poids

Limites :

  • Incapacité à gérer des raisonnements complexes
  • Génération peu fiable de texte long et cohérent
  • Moins de profondeur factuelle que les modèles de production

L'intérêt principal : comprendre le fonctionnement interne des LLM.

Composants clés : comment fonctionne réellement un LLM

Avant d'écrire du code, il faut comprendre les quatre principales parties.

Tokeniseur

Le tokeniseur convertit le texte brut en ID entiers. Par exemple, "Bonjour, le monde !" devient quelque chose comme [15496, 11, 995, 0]. Chaque entier correspond à une unité de sous-mot d'un vocabulaire fixe.

Impact sur les API : le nombre de jetons impacte la latence et le coût. Comprendre la découpe du texte par le tokeniseur vous aide à rédiger des prompts qui rentrent dans la fenêtre de contexte et à éviter la troncature.

GuppyLM utilise un tokeniseur caractère. Les modèles de production (GPT-4, etc.) utilisent le BPE (byte-pair encoding) avec des vocabulaires de 50K-100K jetons.

Couche d'intégration (embedding)

La couche d'intégration convertit les ID de jetons en vecteurs denses (par exemple, 384 dimensions dans GuppyLM). Ces vecteurs portent un sens sémantique : les jetons similaires sont proches dans l'espace vectoriel.

Des intégrations de position sont ajoutées pour que le modèle connaisse l'ordre des jetons.

Blocs Transformeur

Le cœur du calcul. Chaque bloc a deux parties :

Auto-attention : chaque jeton examine tous les autres jetons de la séquence pour décider lesquels sont importants pour prédire le jeton suivant. GuppyLM utilise 6 têtes d'attention sur 6 couches.

Réseau feed-forward : un MLP à deux couches appliqué à chaque jeton après l'attention. GuppyLM utilise une activation ReLU (plus simple que le SwiGLU des architectures récentes).

Tête de sortie

Après le dernier bloc de transformateur, une couche linéaire projette la représentation de chaque jeton vers un vecteur de taille égale au vocabulaire. On applique softmax pour obtenir des probabilités, on choisit le jeton suivant (ou on échantillonne), et on recommence.

Construire un LLM minimal en Python

Voici un LLM minimal fonctionnel, inspiré de GuppyLM, en PyTorch standard :

import torch
import torch.nn as nn
import torch.nn.functional as F

# Hyperparamètres
VOCAB_SIZE = 256     # niveau caractère : un emplacement par caractère ASCII
D_MODEL = 128        # dimension d'intégration (embedding)
N_HEADS = 4          # têtes d'attention
N_LAYERS = 3         # blocs transformeur
SEQ_LEN = 64         # fenêtre de contexte
DROPOUT = 0.1

class SelfAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.n_heads = n_heads
        self.head_dim = d_model // n_heads
        self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
        self.proj = nn.Linear(d_model, d_model, bias=False)
        self.dropout = nn.Dropout(DROPOUT)

    def forward(self, x):
        B, T, C = x.shape
        qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim)
        q, k, v = qkv.unbind(dim=2)
        q = q.transpose(1, 2)
        k = k.transpose(1, 2)
        v = v.transpose(1, 2)
        # Masque causal : chaque jeton ne peut faire attention qu'aux jetons précédents
        scale = self.head_dim ** -0.5
        attn = (q @ k.transpose(-2, -1)) * scale
        mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool()
        attn = attn.masked_fill(mask, float('-inf'))
        attn = F.softmax(attn, dim=-1)
        attn = self.dropout(attn)
        out = (attn @ v).transpose(1, 2).reshape(B, T, C)
        return self.proj(out)

class TransformerBlock(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.attn = SelfAttention(d_model, n_heads)
        self.ff = nn.Sequential(
            nn.Linear(d_model, 4 * d_model),
            nn.ReLU(),
            nn.Linear(4 * d_model, d_model),
            nn.Dropout(DROPOUT),
        )
        self.ln1 = nn.LayerNorm(d_model)
        self.ln2 = nn.LayerNorm(d_model)

    def forward(self, x):
        x = x + self.attn(self.ln1(x))
        x = x + self.ff(self.ln2(x))
        return x

class TinyLLM(nn.Module):
    def __init__(self):
        super().__init__()
        self.embed = nn.Embedding(VOCAB_SIZE, D_MODEL)
        self.pos_embed = nn.Embedding(SEQ_LEN, D_MODEL)
        self.blocks = nn.ModuleList([
            TransformerBlock(D_MODEL, N_HEADS) for _ in range(N_LAYERS)
        ])
        self.ln_f = nn.LayerNorm(D_MODEL)
        self.head = nn.Linear(D_MODEL, VOCAB_SIZE, bias=False)

    def forward(self, idx):
        B, T = idx.shape
        tok_emb = self.embed(idx)
        pos = torch.arange(T, device=idx.device)
        pos_emb = self.pos_embed(pos)
        x = tok_emb + pos_emb
        for block in self.blocks:
            x = block(x)
        x = self.ln_f(x)
        logits = self.head(x)
        return logits

# Initialiser et compter les paramètres
model = TinyLLM()
total_params = sum(p.numel() for p in model.parameters())
print(f"Taille du modèle : {total_params:,} paramètres")  # ~1.2M
Enter fullscreen mode Exit fullscreen mode

Boucle d'entraînement

import torch.optim as optim

def train(model, data, epochs=100, lr=3e-4):
    optimizer = optim.AdamW(model.parameters(), lr=lr)
    model.train()
    for epoch in range(epochs):
        # data: tenseur d'ID de jetons, forme [batch, seq_len+1]
        x = data[:, :-1]   # entrée : tous les jetons sauf le dernier
        y = data[:, 1:]    # cible : tous les jetons décalés de 1
        logits = model(x)
        loss = F.cross_entropy(logits.reshape(-1, VOCAB_SIZE), y.reshape(-1))
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if epoch % 10 == 0:
            print(f"Époque {epoch}, perte : {loss.item():.4f}")
Enter fullscreen mode Exit fullscreen mode

Inférence (génération de texte)

@torch.no_grad()
def generate(model, prompt_ids, max_new_tokens=50, temperature=1.0, top_k=10):
    model.eval()
    ids = torch.tensor([prompt_ids])
    for _ in range(max_new_tokens):
        idx_cond = ids[:, -SEQ_LEN:]  # rogner à la fenêtre de contexte
        logits = model(idx_cond)
        logits = logits[:, -1, :] / temperature  # dernier jeton seulement
        # échantillonnage top-k
        v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
        logits[logits < v[:, [-1]]] = float('-inf')
        probs = F.softmax(logits, dim=-1)
        next_id = torch.multinomial(probs, num_samples=1)
        ids = torch.cat([ids, next_id], dim=1)
    return ids[0].tolist()
Enter fullscreen mode Exit fullscreen mode

Ce que cela vous apprend sur le comportement des API d'IA

Construire un LLM vous rend plus efficace avec les API d'IA. Voici les enseignements essentiels :

La température et l'échantillonnage sont des mécaniques

La température divise les logits avant softmax.

  • Température élevée = plus aléatoire
  • Température basse = plus déterministe

Avec temperature=0.0, la plupart des API planchent la valeur pour éviter les sorties dégénérées. Si vos résultats sont inconsistants à température zéro, ce n'est pas un bug.

Les fenêtres de contexte sont des limites strictes

La ligne idx_cond = ids[:, -SEQ_LEN:] dans l'inférence montre la réalité : les jetons hors contexte sont supprimés. Votre API n'a plus l'historique complet au bout d'un moment. Voir [internal: how-ai-agent-memory-works] pour plus de détails.

Le streaming de jetons = étapes d'inférence visibles

Les API de streaming exécutent la boucle d'inférence et transmettent chaque jeton au flux en temps réel. Si le flux est interrompu, il faut recommencer : impossible de reprendre une génération coupée.

Les logits expliquent la difficulté de la sortie structurée

À chaque étape, le modèle attribue une probabilité à chaque jeton. Générer un JSON valide exige que chaque jeton correct soit sélectionné à chaque position. Des bibliothèques comme Outlines et Guidance contraignent la distribution pour garantir la grammaire. Les modes de "sortie structurée" des API font exactement cela en interne.

Comment tester les intégrations d'API d'IA avec Apidog

Avec une compréhension claire de l'inférence LLM, vous pouvez écrire de meilleurs tests d'API.

Les scénarios de test d'Apidog permettent de chaîner les appels API et d'affirmer la structure des réponses IA.

Exemple pour tester une API de chat en streaming :

  1. Créez un scénario de test dans Apidog avec le point de terminaison /v1/chat/completions
  2. Définissez des assertions sur la réponse : response.choices[0].finish_reason == "stop", response.usage.total_tokens < 4096
  3. Ajoutez une étape pour envoyer la réponse comme contexte au tour suivant (multi-tours)
  4. Utilisez Smart Mock d'Apidog pour simuler des cas d'erreur : finish_reason: "length" (sortie tronquée), finish_reason: "content_filter", ou un timeout réseau en cours de flux

Cela permet de tester les intégrations IA sans consommer de crédits API à chaque CI. Consultez [internal: api-testing-tutorial] pour un aperçu plus large des stratégies de test API.

Test des assertions de nombre de jetons

{
  "assertions": [
    {
      "field": "response.usage.completion_tokens",
      "operator": "less_than",
      "value": 512
    },
    {
      "field": "response.choices[0].finish_reason",
      "operator": "equals",
      "value": "stop"
    },
    {
      "field": "response.choices[0].message.content",
      "operator": "not_empty"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Exécutez ce scénario sur plusieurs modèles (GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro) pour détecter les différences de schéma avant la mise en production.

Avancé : Quantification et optimisation de l'inférence

Lorsque votre LLM minimal fonctionne, deux optimisations sont à connaître :

Quantification

Les poids sont en float32 par défaut. La quantification réduit à INT8 ou INT4, divisant la mémoire utilisée par 4 à 8 avec une perte de précision modérée.

# Quantification INT8 dynamique dans PyTorch
import torch.quantization
quantized_model = torch.quantization.quantize_dynamic(
    model, {nn.Linear}, dtype=torch.qint8
)
Enter fullscreen mode Exit fullscreen mode

Les API de production servent des modèles quantifiés. Une différence de qualité entre versions de modèle peut venir de la quantification.

Cache KV

Dans la boucle d'inférence ci-dessus, l'attention est recalculée sur toute la séquence à chaque étape. En production, les clés/valeurs des jetons précédents sont mises en cache (cache KV) : chaque nouveau jeton ne nécessite que le calcul d'attention du dernier token. C'est pourquoi le premier jeton d'une réponse en streaming est plus lent que les suivants.

Petit LLM vs. API de production : quand utiliser lequel

Cas d'utilisation Petit LLM API de production
Apprendre le fonctionnement interne Idéal Exagéré
Prototypage d'une nouvelle application Qualité insuffisante Idéal
Données privées/sensibles Bonne option Dépend du fournisseur
Déploiement hors ligne/en périphérie Viable Non possible
Coût-sensible, gros volume Possible avec compromis Coûteux à grande échelle
Raisonnement complexe requis Non viable Requis

La plupart du temps : utilisez l'API de production, mais exécutez un petit LLM pour comprendre ce qui se passe sous le capot. Les deux approches sont complémentaires. Consultez [internal: open-source-coding-assistants-2026] pour explorer les assistants open source "apportez votre propre modèle".

Conclusion

Construire un petit LLM à partir de zéro prend un week-end. Ce n'est pas un système de production, mais vous comprendrez vraiment comment fonctionnent tous les LLM, de GuppyLM à GPT-4o. Cette compréhension est précieuse pour déboguer, ajuster les paramètres d'échantillonnage, ou écrire des assertions pour vos tests d'API IA.

GuppyLM est un excellent point de départ : clonez-le, entraînez-le sur un dataset textuel, et plongez dans la boucle d'inférence. Ensuite, vos intégrations d'API de production ne vous paraîtront plus jamais opaques.

Testez vos intégrations d'API d'IA avec les Scénarios de Test d'Apidog pour la même rigueur que pour tout autre backend.

FAQ

Combien de paramètres un "petit" LLM a-t-il besoin pour générer du texte cohérent ?

Environ 10M-50M de paramètres avec un jeu de données d'entraînement décent permettent des phrases localement cohérentes. En dessous de 1M, c'est souvent du charabia. GuppyLM (8,7M) fonctionne pour des conversations courtes sur son domaine d'entraînement (60 sujets).

Puis-je exécuter un petit LLM sans GPU ?

Oui. Les modèles <100M de paramètres fonctionnent sur CPU, plus lentement. Le modèle ci-dessus (1,2M) génère des jetons en millisecondes sur un CPU d'ordinateur portable.

Quel jeu de données utiliser pour l'entraînement ?

Les modèles caractère fonctionnent bien avec le Projet Gutenberg, des sous-ensembles Wikipédia ou tout corpus texte brut. GuppyLM utilise un dataset de conversations (60K entrées, HuggingFace : arman-bd/guppylm-60k-generic). Pour du code, utilisez The Stack ou CodeParrot.

Différence entre température et top-k ?

La température module la distribution des logits (aléatoire globale). Top-k restreint l'échantillonnage aux k jetons les plus probables avant d'appliquer la température. On applique d'abord top-k, puis la température.

Pourquoi mon LLM se répète-t-il parfois ?

La répétition vient du modèle qui donne une forte proba aux jetons récemment générés. Les API de production appliquent des pénalités de répétition (repetition_penalty=1.1).

Temps d'entraînement d'un petit LLM ?

Le modèle ci-dessus s'entraîne en moins de 2h sur un GPU (RTX 3060 ou équivalent). GuppyLM : similaire dans Colab. Les modèles plus gros (>100M) nécessitent multi-GPU et plusieurs jours.

Passer à un endpoint API ?

Exportez au format GGUF via llama.cpp, puis servez-le avec llama-server pour un endpoint API compatible OpenAI local. Pointez Apidog dessus pour les tests ([internal: rest-api-best-practices]).

Comment les LLM de production gèrent-ils un contexte plus long que leur fenêtre ?

Techniques : RoPE (Rotary Position Embedding) avec mise à l'échelle, attention glissante, RAG. L'architecture de base reste un transformeur ; ce sont des modifications sur l'encodage de position et la fenêtre d'attention.


Images, vidéos, et liens originaux conservés.

Top comments (0)