Les coûts de l'inférence
- Leçon 22
- 55 min
- Module 7 : Utiliser des modèles préentraînés
Leçon 22 sur 23 · L'apprentissage profond et les transformeurs expliquésLes coûts de l'inférence
1. Ce qu'est un réseau de neurones
2. Les tenseurs et PyTorch
3. Comment les réseaux de neurones apprennent
4. Diagnostiquer les problèmes d'entraînement
5. Représenter les séquences et le sens
6. Assembler un transformeur
7. Utiliser des modèles préentraînés
8. Sous le capot : les mathématiquesFacultatif
Ce que vous saurez faire
Avant de déployer un modèle, sachez s’il tient en mémoire et à quelle vitesse il répond. Vous estimez la mémoire de SmolLM2-135M à partir de son nombre de paramètres. Vous voyez ce que le cache KV conserve et ce qu’il économise. Vous examinez aussi des mesures réelles des lots et de la quantification. Vous pourrez ensuite expliquer pourquoi l’entraînement demande beaucoup plus de mémoire que l’inférence.
Carnet JupyterFaites cette leçon dans un carnet Jupyter
Le carnet (notebook) Jupyter calcule la mémoire des poids et examine le contenu du cache KV. Il mesure le temps de génération sur votre ordinateur, avec et sans cache, puis avec un lot. Il essaie aussi la quantification int8 et compte la mémoire d’une véritable étape d’entraînement. Environ 30 minutes. La dernière section demande environ 3 Go de mémoire libre.
Le carnet est en anglais. Colab et Kaggle ouvrent votre propre copie. Il faut un compte Google ou Kaggle gratuit. Vous êtes bloqué ? Consultez le carnet de solution.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- SmolLM2-135M, ses 30 blocs, l’attention à requêtes groupées et la boucle de génération, vus dans Entraînement et inférence.
- Les requêtes, les clés et les valeurs, vues dans L’attention étape par étape.
- Le type de données
float32, vu dans Tenseurs et formes. - Facultatif : la latence, le débit et le principe de la quantification, vus dans Les compromis sur les ressources du cours sur les modèles de langage.
Ce qu’il vous faut sur votre ordinateur
- Rien si vous utilisez le carnet.
- Pour travailler localement : la configuration d’Entraînement et inférence, avec SmolLM2-135M chargé dans
modelettokenizer, etPROMPTetshow_shape()définis comme dans cette leçon.
Le cours a mesuré les durées de cette page sur un Mac mini peu sollicité, avec PyTorch 2.14.1 et transformers 5.19.0. Il a utilisé le processeur avec 4 fils d’exécution et retenu la médiane de 5 essais. Vos durées seront différentes; les comparaisons devraient rester les mêmes.
Relire une lettre pour chaque mot
Section intitulée « Relire une lettre pour chaque mot »Imaginez que vous rédigez une réponse à une longue lettre, avec une règle étrange. Avant chaque nouveau mot, vous relisez toute la lettre depuis le début, puis votre réponse jusqu’ici. Le premier mot arrive vite. Au cinquantième mot, vous avez tout lu cinquante fois. Une personne raisonnable lit la lettre une fois, prend des notes et y ajoute une ligne pour chaque mot écrit.
Les notes ont un coût : elles occupent de l’espace sur le bureau, et cet espace augmente avec chaque mot. La génération avec un modèle de langage présente le même compromis.
Le problème : tiendra-t-il en mémoire, et à quelle vitesse répondra-t-il?
Section intitulée « Le problème : tiendra-t-il en mémoire, et à quelle vitesse répondra-t-il? »Amira doit exécuter SmolLM2-135M dans l’application de soutien. Avant de demander un serveur, elle veut trois chiffres. De combien de mémoire le modèle a-t-il besoin? Combien de temps prend une réponse? Que se passe-t-il quand dix agents de soutien l’utilisent en même temps? Si elle devine, l’application risque de planter par manque de mémoire. Elle risque aussi de gaspiller de l’argent sur un serveur beaucoup trop gros.
Vous pouvez obtenir l’essentiel de la réponse par des calculs, avant même d’exécuter quoi que ce soit.
Mémoire, partie 1 : les poids
Section intitulée « Mémoire, partie 1 : les poids »Chaque paramètre est un nombre, et chaque nombre occupe un nombre fixe d’octets. float32 (fp32) occupe 4 octets. bfloat16 (bf16) est un format de 16 bits ayant la même plage que float32, mais moins de chiffres : 2 octets. int8 occupe 1 octet, et int4, un demi-octet. Donc :
| Format | Octets par paramètre | SmolLM2-135M (134 515 008 paramètres) |
|---|---|---|
| float32 | 4 | 538 Mo |
| bfloat16 | 2 | 269 Mo |
| int8 | 1 | 135 Mo |
| int4 | 0,5 | 67 Mo |
Les tenseurs réels confirment le calcul. Dans le carnet, le modèle chargé en float32 contient exactement 538 060 032 octets de poids : 134 515 008 × 4.
Les créateurs de SmolLM2 conservent ses poids en bfloat16; voilà pourquoi le téléchargement fait environ 270 Mo. Le cours les charge en float32. La copie en mémoire est donc deux fois plus grosse, mais contient exactement les mêmes valeurs. Il y a deux raisons. Tous les processeurs calculent bien en float32, et toutes les mesures du cours ont été faites en float32.
Mémoire, partie 2 : les activations
Section intitulée « Mémoire, partie 2 : les activations »Les activations sont les nombres qu’une propagation avant calcule en cours de route. (Le mot désigne aussi des fonctions comme ReLU, vues dans Pourquoi les couches ont besoin d’activations; ici, il désigne les sorties des couches.) Ce sont les états cachés après chaque bloc, les scores d’attention et les logits. Elles existent seulement pendant la propagation. En inférence, on peut libérer les activations d’un bloc dès que le bloc suivant les a utilisées. Elles augmentent avec le nombre de textes et de jetons (tokens).
Pour la requête (prompt) de 25 jetons, un état caché a la forme (1, 25, 576) : 57 600 octets en float32. Les logits sont beaucoup plus gros, (1, 25, 49152) : 4 915 200 octets, soit presque 5 Mo. C’est parce que chaque position reçoit un score pour chaque jeton du vocabulaire. Les scores d’attention d’un bloc, 9 têtes × 25 × 25, occupent très peu d’espace pour 25 jetons. Mais ils augmentent avec le carré de la longueur. Les activations deviennent donc importantes avec les longs textes.
Mémoire, partie 3 : le cache KV
Section intitulée « Mémoire, partie 3 : le cache KV »Rappelez-vous l’attention : on compare la requête de chaque jeton aux clés des jetons précédents, puis les poids combinent leurs valeurs. Avec le masque causal, les clés et les valeurs des jetons précédents ne changent jamais lorsqu’un nouveau jeton arrive. Il est donc inutile de les recalculer. Le cache KV conserve les clés et les valeurs de tous les jetons déjà traités, dans chaque bloc. C’est comme les notes sur le bureau.
Cela divise la génération en deux phases. Le préremplissage (prefill) fait passer toute la requête dans le modèle une fois et remplit le cache. Ensuite, chaque étape de décodage fournit seulement le jeton le plus récent. La requête de ce jeton lit les clés et les valeurs en cache. On ajoute ensuite sa propre clé et sa propre valeur au cache.
Description textuelle du diagramme
La rangée du haut montre les phases. D’abord, le préremplissage, une seule fois : les 25 jetons de la requête traversent le modèle en une propagation avant. Cela prend environ 0,02 seconde. Puis l’étape de décodage 1 : le jeton d’entrée est « is », et la sortie est « very », en environ 0,011 seconde. Étape de décodage 2 : entrée « very », sortie « helpful ». Étape de décodage 3 : entrée « helpful », sortie un point. Cela se répète jusqu’à 40 nouveaux jetons ou un jeton de fin.
Sous chaque phase se trouve le cache KV. Après le préremplissage, il contient 25 jetons : forme (1, 3, 25, 64) dans chacun des 30 blocs. Chaque étape de décodage lit le cache et ajoute un jeton : 26, 27, puis 28 jetons.
En bas, en pointillés : sans cache, chaque étape exécute de nouveau le modèle sur tout le texte. Il traite 25 jetons, puis 26, 27, jusqu’à 64 jetons pour produire 40 nouveaux jetons. La réponse reste la même, mais le travail augmente à chaque étape : 1,01 seconde plutôt que 0,46 seconde sur le processeur du cours.
Amira examine le contenu. use_cache=True demande au modèle de renvoyer le cache dans past_key_values après le préremplissage :
inputs = tokenizer(PROMPT, return_tensors="pt")with torch.no_grad(): out = model(**inputs, use_cache=True) # the prefillcache = out.past_key_valuesprint("blocks in the cache:", len(cache.layers))show_shape("keys", cache.layers[0].keys)show_shape("values", cache.layers[0].values)cache_bytes = sum(l.keys.numel() * 4 + l.values.numel() * 4 for l in cache.layers)print("cache bytes for 25 tokens:", cache_bytes, "| per token:", cache_bytes // 25)La sortie :
blocks in the cache: 30keys shape (1, 3, 25, 64)values shape (1, 3, 25, 64)cache bytes for 25 tokens: 1152000 | per token: 46080Lisez la forme (1, 3, 25, 64) ainsi : 1 texte, 3 têtes clé-valeur, 25 jetons, 64 nombres. Puis faites une étape de décodage, avec seulement le jeton le plus récent en entrée :
next_id = out.logits[0, -1].argmax().reshape(1, 1)with torch.no_grad(): step = model(input_ids=next_id, past_key_values=cache, use_cache=True)show_shape("input", next_id)show_shape("logits", step.logits)show_shape("keys now", cache.layers[0].keys)La sortie :
input shape (1, 1)logits shape (1, 1, 49152)keys now shape (1, 3, 26, 64)Un jeton en entrée, une ligne de scores en sortie, et le cache a grandi d’un jeton.
Quelle est la taille du cache?
Section intitulée « Quelle est la taille du cache? »Chaque jeton conserve une clé et une valeur (2), dans 30 blocs, pour 3 têtes clé-valeur de 64 nombres, à 4 octets chacun :
2 × 30 × 3 × 64 × 4 = 46 080 octets par jeton.
La requête de 25 jetons demande 1 152 000 octets, exactement la taille du cache réel de PyTorch ci-dessus. Un texte de 1 024 jetons demande 47 Mo. Au contexte maximal du modèle, 8 192 jetons, il demande 377 Mo : plus que les poids en int8. Et chaque texte en cours d’utilisation a besoin de son propre cache.
C’est ici que l’attention à requêtes groupées devient avantageuse. Avec une tête clé-valeur pour chacune des 9 têtes de requête, le cache serait trois fois plus gros. Il occuperait 138 240 octets par jeton, soit 1,13 Go pour 8 192 jetons.
- Poids
- Cache KV
Description textuelle du graphique
Un diagramme à colonnes en mégaoctets. Poids : float32 538, bfloat16 269, int8 135, int4 67. Cache KV d’un texte en float32 : 47 pour 1 024 jetons et 377 pour 8 192 jetons.
Ce que le cache économise
Section intitulée « Ce que le cache économise »Le cours a mesuré la génération gloutonne de 40 nouveaux jetons pour la requête de Larkfield, avec et sans cache. Le préremplissage des 25 jetons de la requête a pris environ 0,02 seconde, et une étape de décodage, environ 0,011 seconde.
Description textuelle du graphique
Un diagramme à colonnes du nombre de secondes pour 40 nouveaux jetons. Avec le cache KV : 0,46. Sans cache : 1,01. Poids int8 avec le cache : 0,60.
Sans cache, les mêmes 40 jetons ont pris 1,01 s plutôt que 0,46 s, et les jetons étaient identiques. L’écart augmente avec la longueur du texte, car chaque étape sans cache relit tous les jetons précédents. Le cache coûte de la mémoire, comme le montre le graphique ci-dessus. Les bibliothèques de génération utilisent le cache par défaut. use_cache=False sert à comparer, ou à économiser de la mémoire quand un texte est très long.
Traitement par lots : plusieurs textes, une seule propagation
Section intitulée « Traitement par lots : plusieurs textes, une seule propagation »Une étape de décodage pour un texte fait peu de calculs par poids lu en mémoire. Le processeur passe donc une grande partie de l’étape à lire les poids. Un lot de textes traverse le modèle ensemble : chaque poids est lu une fois et utilisé pour tous les textes. La mesure du cours a généré 20 jetons pour 8 copies de la requête :
| 8 requêtes × 20 nouveaux jetons | Durée |
|---|---|
| L’une après l’autre | 1,92 s |
| En un seul lot de 8 | 0,47 s |
Le lot était quatre fois plus rapide pour le même travail. Les coûts : les textes de longueurs différentes nécessitent du remplissage (des jetons ajoutés, à gauche pour la génération). Chaque texte d’un lot attend le lot entier, et le lot a besoin de huit caches. Pour Amira, les lots comptent quand plusieurs agents de soutien font une demande en même temps, pas pour un seul agent.
Quantification : mémoire, vitesse et qualité
Section intitulée « Quantification : mémoire, vitesse et qualité »La quantification conserve les poids avec moins de bits : elle arrondit chaque poids à une valeur parmi un petit ensemble. Les compromis sur les ressources, dans le cours sur les modèles de langage, explique cet arrondi. Cette leçon montre aussi qu’une copie plus petite n’est pas toujours aussi bonne. Ici, le cours a mesuré ses effets sur SmolLM2 avec un processeur.
La quantification dynamique int8 de PyTorch (quantize_dynamic) conserve les poids de chaque couche Linear sous forme d’entiers de 8 bits. Pendant le calcul, elle convertit les nombres nécessaires. Voici les résultats, comparés à float32 :
| float32 | int8 dynamique | |
|---|---|---|
Poids des couches Linear |
4 octets chacun | 1 octet chacun |
| 40 nouveaux jetons | 0,46 s | 0,60 s |
| Probabilité que « is » suive | 0,107 | 0,195 |
| Texte glouton | « is very helpful. I am looking for a new parcel. … » | « is not answering my emails. The answer is that … » |
Trois leçons dans un seul tableau. Int8 économise de la mémoire. Il était plus lent sur ce processeur : reconvertir les nombres prend du temps. Le code int8 de ce processeur n’est pas plus rapide que son code float32. Et il a changé les sorties : les probabilités ont changé, donc le texte glouton a pris un autre chemin dès le début.
Même bfloat16, un changement beaucoup plus petit, modifie les nombres. En bfloat16, SmolLM2 donne à « is » une probabilité de 0,102 plutôt que 0,107. Son texte glouton reste identique pendant 10 jetons. Ensuite, il dit « a new home » là où float32 dit « a new parcel ».
La quantification impose donc un compromis à trois dimensions : mémoire, vitesse et qualité. Mesurez les trois sur votre propre matériel et votre propre tâche avant de choisir.
L’entraînement demande beaucoup plus de mémoire que l’inférence
Section intitulée « L’entraînement demande beaucoup plus de mémoire que l’inférence »L’inférence nécessite les poids, les activations de la propagation en cours et le cache. L’entraînement nécessite davantage, pour chaque poids :
- le poids lui-même;
- son gradient issu de la rétropropagation, de même taille que les poids;
- l’état de l’optimiseur. Adam conserve deux nombres supplémentaires par poids : une moyenne mobile des gradients et une moyenne mobile de leurs carrés.
Cela fait quatre nombres pour chaque paramètre. Priya le vérifie avec une véritable étape d’entraînement sur SmolLM2 en float32. Elle prédit le jeton suivant sur la requête et compte les octets :
model.train()optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)batch = tokenizer(PROMPT, return_tensors="pt")loss = model(**batch, labels=batch["input_ids"]).loss # next-token prediction on the promptloss.backward()optimizer.step()
mb = lambda tensors: sum(t.numel() * t.element_size() for t in tensors) / 1e6weights = mb(model.parameters())gradients = mb(p.grad for p in model.parameters())adam = mb(v for state in optimizer.state.values() for k, v in state.items() if k in ("exp_avg", "exp_avg_sq"))print(f"weights {weights:.0f} MB | gradients {gradients:.0f} MB | Adam {adam:.0f} MB | total {weights + gradients + adam:.0f} MB")La sortie :
weights 538 MB | gradients 538 MB | Adam 1076 MB | total 2152 MBlabels= fait calculer au modèle sa propre perte de prédiction du jeton suivant. optimizer.state contient les deux moyennes mobiles d’Adam, exp_avg et exp_avg_sq, pour chaque tenseur de poids. 2 152 Mo : quatre fois les poids. L’entraînement conserve aussi les activations de chaque bloc jusqu’à ce que la rétropropagation les ait utilisées. Elles augmentent avec la taille du lot et la longueur du texte. L’inférence avec le même modèle demande 538 Mo, plus un cache de quelques Mo.
Erreur courante
Un grand modèle entraîné sur du matériel ordinaire
Symptôme : Tomás propose : « Entraînons notre propre grand modèle de langage sur le Mac du bureau. Il exécute SmolLM2 facilement, donc il peut aussi entraîner un gros modèle. » Son plan prévoit un modèle de 70 milliards de paramètres et un Mac avec 16 Go de mémoire.
Cause : la mémoire d’inférence n’est pas la mémoire d’entraînement. L’entraînement en float32 demande quatre nombres de 4 octets par paramètre : le poids, son gradient et les deux états d’Adam. Pour 70 milliards de paramètres, cela donne 70 000 000 000 × 16 octets = 1 120 Go, sans compter les activations. C’est 70 fois la mémoire du Mac. Même SmolLM2-135M demande 2,15 Go pour les nombres d’une étape d’entraînement, contre 538 Mo pour l’exécuter. Et la mémoire n’est pas la seule limite. Les créateurs de SmolLM2 ont utilisé 64 gros processeurs graphiques pour ses 2 000 milliards de jetons de préentraînement.
Solution : calculez la mémoire avant de planifier : paramètres × octets × copies. Le matériel ordinaire peut exécuter de petits modèles et faire leur réglage fin. Pour un grand modèle, adaptez un modèle préentraîné avec des requêtes ou une méthode de réglage fin entraînant seulement quelques poids. L’étude de cas Faire le réglage fin d’un petit transformeur le fait avec un petit modèle.
Ce que cela signifie pour un vrai service d’IA : le matériel qui sert un modèle est généralement beaucoup plus modeste que celui qui l’a entraîné. Prévoyez des budgets séparés.
Quand chaque coût compte
Section intitulée « Quand chaque coût compte »- Les poids déterminent si le modèle tient en mémoire. Calculez-les d’abord : paramètres × octets.
- Le cache KV détermine combien de textes vous pouvez traiter simultanément, et de quelle longueur. Il augmente avec chaque jeton de chaque texte actif.
- Les activations comptent pour les longs textes et les gros lots, et beaucoup plus pendant l’entraînement.
- Le traitement par lots augmente le travail accompli par seconde quand plusieurs requêtes arrivent ensemble. Il n’accélère pas une requête individuelle.
- La quantification économise de la mémoire; elle n’économise pas forcément du temps, et elle change les sorties. Mesurez de nouveau la qualité après l’avoir appliquée.
Exercice guidé : le budget mémoire d’Amira
Section intitulée « Exercice guidé : le budget mémoire d’Amira »- Calculez les poids de SmolLM2-135M en bfloat16 : 134 515 008 × 2 octets. Vous devriez obtenir environ 269 Mo.
- Calculez le cache KV en float32 pour un texte de 1 024 jetons : 46 080 × 1 024. Vous devriez obtenir environ 47 Mo.
- Amira prévoit jusqu’à 10 agents de soutien à la fois, chacun avec un texte d’au plus 1 024 jetons. Calculez le cache pour les 10.
- Additionnez les poids en float32 (538 Mo) et les 10 caches.
- Dans le carnet, exécutez la section 3 et vérifiez que le cache de la requête de 25 jetons contient 1 152 000 octets.
Afficher la réponse
Dix caches de 47,2 Mo font 472 Mo. Avec les poids en float32 : 538 + 472 = environ 1 010 Mo, soit environ 1 Go. Il faut ajouter les activations de la propagation en cours et le programme lui-même. Avec des poids et un cache en bfloat16, les deux diminuent de moitié : environ 505 Mo. Un serveur ayant 2 Go de mémoire libre suffit; vérifiez-le avec un vrai test avant d’acheter.
À vous de jouer : un long fil de discussion
Section intitulée « À vous de jouer : un long fil de discussion »Certains billets de Larkfield deviennent de longs fils de discussion. Faites une prédiction, puis calculez : quelle taille occupe le cache KV en float32 pour un texte de 8 192 jetons? C’est la fenêtre de contexte complète. Refaites le calcul pour un modèle avec une tête clé-valeur par tête de requête (9 plutôt que 3).
Afficher la réponse
46 080 × 8 192 = 377 487 360 octets, soit environ 377 Mo, ou 70 % des poids en float32. Avec 9 têtes clé-valeur, chaque jeton stocke 2 × 30 × 9 × 64 × 4 = 138 240 octets. Cela donne 1 132 462 080 octets pour 8 192 jetons : 1,13 Go, soit plus du double des poids. L’attention à requêtes groupées réduit le cache au tiers.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Que stocke le cache KV, pourquoi peut-on réutiliser ces nombres sans risque, et quel est son coût?
Afficher la réponse
Il stocke les clés et les valeurs de tous les jetons déjà traités, dans chaque bloc. Pour SmolLM2, la forme est (1, 3, jetons, 64) par bloc, soit 46 080 octets par jeton en float32. On peut les réutiliser sans risque grâce au masque causal. Un nouveau jeton ne modifie jamais les clés et les valeurs des jetons précédents. Le coût est la mémoire, qui augmente avec chaque jeton de chaque texte actif : 377 Mo pour un texte de 8 192 jetons.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Faites une prédiction : Amira quantifie SmolLM2 en int8 pour accélérer les réponses du soutien sur le processeur du bureau. Que peut-il arriver à la mémoire, à la vitesse et aux réponses?
Afficher la réponse
Les poids des couches Linear occupent le quart de la mémoire. La vitesse peut ne pas s’améliorer. Sur le processeur du cours, 40 jetons ont pris 0,60 s en int8 contre 0,46 s en float32. Les réponses changent : la probabilité de « is » est passée de 0,107 à 0,195. Le texte généré par choix glouton a suivi un autre chemin. Elle doit mesurer à nouveau la vitesse et la qualité sur son matériel et ses billets.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Pourquoi l'entraînement de SmolLM2 en float32 exige-t-il environ 2,15 Go pour ses nombres, contre environ 538 Mo pour l'inférence? Nommez les ajouts.
Afficher la réponse
L’entraînement ajoute un gradient pour chaque poids (538 Mo) et les deux moyennes mobiles d’Adam pour chaque poids (1 076 Mo). Cela donne quatre copies : 4 × 538 Mo = 2 152 Mo. L’entraînement conserve aussi les activations de chaque bloc pour la rétropropagation. L’inférence nécessite seulement les poids, les activations de la propagation en cours et le cache KV.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Poids : paramètres × octets. SmolLM2-135M : 538 Mo en float32, 269 Mo en bfloat16, 135 Mo en int8, 67 Mo en int4.
- Les activations sont temporaires et augmentent avec le nombre de textes × le nombre de jetons. Il faut les conserver pour la rétropropagation pendant l’entraînement.
- Le cache KV conserve les clés et les valeurs pour que chaque étape de décodage calcule seulement le nouveau jeton. Il occupe 46 080 octets par jeton, soit 377 Mo pour 8 192 jetons. Avec ce cache, 40 jetons ont pris 0,46 s, contre 1,01 s sans lui.
- Le traitement par lots de 8 textes a pris 0,47 s au lieu de 1,92 s. La quantification en int8 a économisé de la mémoire, mais a été plus lente sur ce processeur (0,60 s). Elle a aussi modifié les sorties.
- L’entraînement nécessite environ quatre copies des poids (poids, gradients, deux états d’Adam), plus les activations. Cela représente 2,15 Go pour SmolLM2 et environ 1 120 Go pour un modèle de 70 milliards de paramètres.
Ensuite : enregistrez un modèle et son expérience pour qu’une autre personne puisse la reproduire.
Leçon suivante : Enregistrer et examiner les résultats