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.
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.
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 peuvent : - 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
Ils ne peuvent pas : - Gérer des raisonnements complexes - Générer de manière fiable du texte long et cohérent - Égaler la profondeur factuelle des modèles de production
La valeur ne réside pas dans le résultat. Elle réside dans la compréhension que vous obtenez en en construisant un.
Composants clés : comment fonctionne réellement un LLM
Avant d'écrire du code, vous devez savoir ce que font les quatre principales parties.
Tokeniseur
Le tokeniseur convertit le texte brut en ID entiers. "Bonjour, le monde !" devient quelque chose comme [15496, 11, 995, 0]. Chaque entier correspond à une unité de sous-mot d'un vocabulaire fixe.
Pourquoi cela est important pour le travail avec les API : le nombre de jetons affecte directement la latence et le coût. Comprendre comment les tokeniseurs divisent le texte vous aide à écrire des invites qui tiennent dans les fenêtres de contexte et à éviter une troncature inattendue.
GuppyLM utilise un simple tokeniseur au niveau des caractères. Les modèles de production comme GPT-4 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. Chaque jeton reçoit un vecteur appris (par exemple, 384 dimensions dans GuppyLM). Ces vecteurs portent un sens sémantique : les jetons similaires se retrouvent proches les uns des autres dans l'espace vectoriel.
Des intégrations de position sont ajoutées par-dessus, afin que le modèle connaisse l'ordre des jetons.
Blocs Transformeur
C'est le calcul central. Chaque bloc a deux parties :
Auto-attention : permet à chaque jeton d'examiner tous les autres jetons de la séquence et de 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é à la représentation de chaque jeton après l'attention. GuppyLM utilise une activation ReLU, qui est plus simple que le SwiGLU utilisé dans les architectures plus 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. Appliquez softmax pour obtenir des probabilités, choisissez le jeton suivant le plus probable (ou échantillonnez), et répétez.
Construire un LLM minimal en Python
Voici un LLM minimal fonctionnel basé sur l'approche GuppyLM. Il fonctionne dans 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
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}")
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()
Ce que cela vous apprend sur le comportement des API d'IA
Construire cela révèle plusieurs choses qui font de vous un meilleur consommateur d'API.
La température et l'échantillonnage sont mécaniques, pas magiques
La température divise les logits avant softmax. Température plus élevée = distribution plus plate = sortie plus aléatoire. Température plus basse = distribution plus nette = sortie plus déterministe. Lorsque votre API de production renvoie des résultats inconsistants avec temperature=0.0, ce n'est pas un bug. Une température vraiment nulle est un argmax glouton, et de nombreuses API la plancher légèrement pour éviter les sorties dégénérées.
Les fenêtres de contexte sont des limites strictes, pas de douces suggestions
La ligne idx_cond = ids[:, -SEQ_LEN:] dans la boucle d'inférence montre exactement ce qui se passe à la limite du contexte. Le modèle supprime silencieusement les jetons plus anciens. Si votre intégration API suppose que le modèle se souvient de l'historique complet de la conversation, ce n'est pas le cas après un certain point. Voir [internal: how-ai-agent-memory-works] pour savoir comment les agents gèrent ce problème.
Les jetons en streaming ne sont que des étapes d'inférence rendues visibles
Les API de streaming ne font rien de structurellement différent. Elles exécutent la boucle d'inférence et transmettent chaque jeton au flux de réponse au fur et à mesure de sa génération. Comprendre cela aide lorsque vous écrivez une logique de nouvelle tentative : un flux interrompu en cours de génération ne peut pas être repris, il doit redémarrer.
Les logits expliquent pourquoi la sortie structurée est difficile
Le modèle attribue une probabilité à chaque jeton du vocabulaire à chaque étape. Générer un JSON valide exige que le bon jeton gagne à chaque position. Des bibliothèques comme Outlines et Guidance contraignent la distribution des logits pour faire respecter la grammaire au moment de l'inférence. Lorsque vous voyez des API d'IA proposer des modes de "sortie structurée", c'est ce qu'elles font en interne.
Comment tester les intégrations d'API d'IA avec Apidog
Une fois que vous comprenez comment fonctionne l'inférence LLM, vous pouvez écrire de bien meilleurs tests d'API. Les scénarios de test d'Apidog vous permettent d'enchaîner les appels API et d'effectuer des assertions sur la structure des réponses de l'IA.
Par exemple, lors du test d'une API de chat en streaming :
- Créez un scénario de test dans Apidog avec votre point de terminaison
/v1/chat/completions - Définissez des assertions pour vérifier la structure de la réponse :
response.choices[0].finish_reason == "stop",response.usage.total_tokens < 4096 - Ajoutez une étape de suivi qui envoie la réponse comme contexte au tour suivant, simulant une conversation multi-tours
- Utilisez Smart Mock d'Apidog pour simuler le point de terminaison de l'IA et tester la gestion des erreurs de votre application : simulez
finish_reason: "length"(sortie tronquée),finish_reason: "content_filter"et un timeout réseau en milieu de flux
C'est ainsi que vous testez les intégrations d'IA sans brûler des crédits d'API à chaque exécution CI. Voir [internal: api-testing-tutorial] pour un aperçu plus large des approches de test d'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"
}
]
}
Exécutez ceci sur plusieurs modèles (GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro) dans un seul Scénario de Test pour détecter les différences de schéma d'API avant qu'elles n'atteignent la production.
Avancé : Quantification et optimisation de l'inférence
Une fois que vous avez un petit LLM fonctionnel, deux techniques méritent d'être comprises car elles s'appliquent directement à la façon dont les modèles de production sont servis.
Quantification
Les poids de notre modèle sont des flottants 32 bits par défaut. La quantification les réduit à des entiers 8 bits (INT8) ou même 4 bits (INT4). Cela réduit l'utilisation de la mémoire de 4 à 8 fois avec une perte de précision modeste.
# Exemple : quantification INT8 dynamique dans PyTorch
import torch.quantization
quantized_model = torch.quantization.quantize_dynamic(
model, {nn.Linear}, dtype=torch.qint8
)
Les API de production exécutent des modèles quantifiés. Lorsque vous constatez une qualité de sortie différente selon les "versions" d'un même modèle, la quantification est souvent impliquée.
Cache KV
Dans notre boucle d'inférence, nous recalculons l'attention sur toute la séquence à chaque étape. Les systèmes de production mettent en cache les paires clé-valeur des jetons précédents (le cache KV) afin que chaque nouveau jeton n'ait besoin que d'un nouveau calcul d'attention. C'est pourquoi le premier jeton dans une réponse en streaming prend plus de temps que les suivants.
Petit LLM vs. API de production : quand utiliser lequel
| Cas d'utilisation | Petit LLM | API de production |
|---|---|---|
| Apprendre les mécanismes internes du modèle | Idéal | Exagéré |
| Prototyper 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 des compromis | Coûteux à grande échelle |
| Tâches nécessitant un raisonnement | Non viable | Requis |
La vraie réponse pour la plupart des développeurs : utilisez l'API de production pour votre application, mais exécutez un petit modèle pour comprendre ce qui se passe en coulisses. Les deux ne sont pas en concurrence. L'article [internal: open-source-coding-assistants-2026] couvre les outils qui brouillent cette ligne avec des configurations "apportez votre propre modèle".
Conclusion
Construire un petit LLM à partir de zéro prend un week-end. Ce que vous obtenez n'est pas un système de production ; c'est un modèle mental fonctionnel de la façon dont chaque modèle de langage, de GuppyLM à GPT-4o, fonctionne réellement. Cette compréhension est utile chaque fois que vous déboguez une intégration de streaming, ajustez les paramètres d'échantillonnage ou concevez des assertions pour vos tests d'API d'IA.
Le projet GuppyLM est un bon point de départ. Clonez-le, entraînez-le sur n'importe quel jeu de données textuelles et passez un après-midi à lire la boucle d'inférence. Ensuite, retournez à vos intégrations d'API de production et vous les verrez différemment.
Essayez les Scénarios de Test d'Apidog pour apporter la même rigueur à vos tests d'API d'IA que celle que vous appliqueriez à tout autre système de 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 peuvent produire des phrases localement cohérentes. En dessous de 1M, vous obtenez du charabia pour la plupart des tâches. 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 de moins de 100M de paramètres fonctionnent bien sur CPU, bien que l'inférence soit plus lente. Le modèle ci-dessus (1,2M de paramètres) génère des jetons en millisecondes sur un CPU d'ordinateur portable.
Quel jeu de données devrais-je utiliser pour l'entraînement ?Les modèles au niveau des caractères fonctionnent bien avec les textes du Projet Gutenberg, des sous-ensembles de Wikipédia ou tout corpus de texte brut. GuppyLM utilise un jeu de données de conversation de 60K entrées sur HuggingFace (arman-bd/guppylm-60k-generic). Pour la génération de code, utilisez The Stack ou CodeParrot.
Quelle est la différence entre la température et l'échantillonnage top-k ?La température fait varier la distribution des logits (contrôle le caractère aléatoire général). Le top-k restreint le pool d'échantillonnage aux k jetons les plus probables avant d'appliquer la température. Ils sont appliqués ensemble : d'abord le top-k filtre les candidats, puis la température façonne les probabilités au sein de cet ensemble.
Pourquoi mon LLM se répète-t-il parfois ?La répétition est un mode d'échec où le modèle attribue une forte probabilité aux jetons qu'il vient de générer parce qu'ils sont apparus dans le contexte. Les API de production utilisent des pénalités de répétition (un ajustement des logits qui réduit les jetons récemment générés). Ajoutez repetition_penalty=1.1 dans votre appel d'API pour réduire cela.
Combien de temps faut-il pour entraîner un petit LLM ?Le modèle ci-dessus s'entraîne pour produire une sortie cohérente en moins de 2 heures sur un seul GPU (RTX 3060 ou équivalent). GuppyLM s'entraîne dans Colab en à peu près le même temps. Les modèles plus grands (100M+) nécessitent des configurations multi-GPU et des jours d'entraînement.
Quelle est la manière la plus rapide de passer d'un petit LLM à un véritable point de terminaison d'API ?Exportez au format GGUF en utilisant le script de conversion de llama.cpp, puis servez-le avec llama-server. Cela vous donne un point de terminaison d'API compatible OpenAI fonctionnant localement. Vous pouvez ensuite pointer Apidog dessus pour le tester, voir [internal: rest-api-best-practices].
Comment les LLM de production gèrent-ils un contexte plus long que leur fenêtre d'entraînement ?Des techniques comme le RoPE (Rotary Position Embedding) avec une mise à l'échelle étendue, l'attention à fenêtre glissante et la génération augmentée par la récupération étendent toutes le contexte effectif. L'architecture de transformateur de base ne change pas ; ce sont des modifications de la façon dont les informations de position sont encodées et de la façon dont la fenêtre d'attention est appliquée.
