03-DeepLearning — Le deep learning from scratch : ouvrir la boîte noire couche par couche

← DataScienceWithAgents (série parente) | 02-ML-Cours (prérequis)

Kernel : Python 3 (coursia-ml-training pour 3.4c, 3.7, 3.9d, 3.12 et 3.13) · Bibliothèques : NumPy (implémentations from scratch), matplotlib, torch (moteur principal de 3.6b à 3.13 ; parité ailleurs), torchvision, diffusers (3.10) · Niveau : intermédiaire (post socle ML) · CPU : oui (exceptions : famille compression 3.9a/3.9b/3.9e/3.9f/3.9g, recette complète ResNet-20 sur GPU ~8 min (~30 min pour 3.9b, bras de réentraînement compris) — recette CPU 6 époques sinon ; 3.9d : distillation complète sur GPU ~2 h ; 3.6c ~19 min CPU ; 3.13b : deux cartes CUDA et conteneur Linux requis — nccl n’existe pas sous Windows, exécution via l’image PyTorch officielle, dite comme tel dans le carnet)

Pourquoi cette série

Le socle 02-ML-Cours laisse un chaînon ouvert. La descente de gradient y est ouverte à la main (2.2) — mais sur une droite ; le premier réseau de neurones de la formation (2.9) est entraîné en PyTorch boîte noire : loss.backward() et tout suit. Entre les deux, personne n’a écrit la rétropropagation à la main — l’étape où l’on comprend réellement pourquoi un réseau apprend.

Cette série ouvre cette mécanique, un concept par notebook, avec une discipline constante : from scratch PUIS framework. Chaque mécanisme est d’abord implémenté en NumPy pur (sans autograd), vérifié (gradient numérique par différence finie, parité pas-à-pas avec l’équivalent PyTorch), et seulement ensuite relié à l’API PyTorch que consomment nos autres séries (RL, PostTraining, ML-Training-Pipeline). L’entraînement final du 2.9 et les optimizer = Adam(...) des séries appliquées deviennent lisibles par construction.

Vue d’ensemble

Notebook Sujet Concept-phare Validation
3.0-Theorie-Information Entropie, cross-entropy et KL construites from scratch sur un texte français, puis MSE vs cross-entropy sur un classifieur (le piège du gradient saturé), température (softmax) et pont vers DPO/GRPO La loss qui fait apprendre : pourquoi la cross-entropy (et pas la MSE) est la bonne loss d’un classifieur, et la KL comme mesure de décalage entre deux distributions de modèle entropie du français ~4,4 bits (redondance ~10-15 %, borne ≤ log₂K vérifiée) ; identité H(p,q)=H(p)+D_KL vérifiée, KL>0 sur tout le balayage (Gibbs) ; init saturée et fausse : la CE s’échappe (0,88) quand la MSE reste bloquée (0,39), gradient CE/MSE ~51× ; log 0 maîtrisé par lissage ε ; KL minimale en T=1
3.1-Retropropagation Le MLP et la rétropropagation à la main (NumPy pur, sans autograd) Le gradient vérifié : différence finie vs analytique, parité exacte avec PyTorch écart 1,3e-11 (seuil 1e-6) ; loss initiale, premier pas et trajectoire 3000 iters identiques à 1,1e-16 près ; init nulle = gradient nul (0,500 figé)
3.2-Optimisateurs Momentum, Adagrad, RMSProp, Adam et schedules, écrits en NumPy pur puis validés pas à pas contre torch.optim La parité exacte : les 5 mises à jour sont celles de torch GD/momentum/Adam à 1,11e-16, Adagrad bit-à-bit (0,00e+00), RMSProp à 2,22e-16 (float64, 1 pas) ; Beale : 5 trajectoires superposées (facteur 200 entre lr utilisables) ; MLP du 3.1 : 5 optimisateurs × 3 graines (RMSProp 0,059 < Adam 0,061 < … < GD 0,070) ; schedules : coût en full-batch déterministe, gain sous le plancher de bruit en mini-batch
3.3-Regularisation Dropout, weight decay et early stopping écrits à la main sur un MLP construit pour surapprendre (17 000 paramètres, 100 points d’entraînement, 12 étiquettes fausses) Corriger la variance sans changer le modèle : trois remèdes appliqués au même surapprentissage fabriqué sans régularisation : val acc 84,0 % (les 12 erreurs mémorisées) ; dropout p=0,3 : 89,0 % ; early stopping : +4,5 % ; gradient check du dropout (masque gelé) et invariants du dropout inverse (p=0,4, 2000×50)
3.4-Attention-Transformer-From-Scratch De l’attention mono-tête lisible sur l’inversion de séquence au mini-GPT de 1,25 M entraîné dans le notebook L’attention jusqu’au bout, sur CPU : attention + masque causal + multi-têtes + bloc pré-norme, équivalence numérique avec torch.nn, mini-GPT char-level entraîné (117 s) sur le Horla (Maupassant, domaine public, 59 k caractères) sac de mots A = B pour phrases opposées (1-9) ; inversion : poids requete 0 = [0,007 0,007 0,007 0,007 0,007 0,967] ; multi-têtes maison vs torch.nn.MultiheadAttention à 2,38e-07 (assert allclose 1e-5) ; mini-GPT 1 251 040 paramètres : perte ln(85)=4,44 → train 2,46 / val 2,45 en 800 étapes (117 s), perplexité val 11,5 ; 4 têtes spécialisées post-entraînement (diagonale 0,110 / 0,172 / 0,133 / 0,102)
3.4c-MoE-from-scratch La couche Mixture of Experts écrite à la main (routeur top-k, capacité par expert avec jetons jetés, loss d’équilibrage E·Σ f_i·P_i), son coût mesuré en millisecondes contre deux FFN denses d’équivalence (iso-calcul, iso-paramètres) puis son entraînement dense contre MoE sur la tâche jouet du 3.4b Découpler paramètres et calcul : E experts dont seuls k travaillent par jeton — le banc entrelacé mesure ce que la promesse vaut réellement, routage compris routeur vérifié sur cas contrôlés (entropie uniforme 2,0794 = ln 8, tie-break de topk exposé) ; banc de coût : MoE E=8 k=2 (1 054 720 params) à 0,75× le temps du dense-32d iso-paramètres mais ~2,9× le dense-8d iso-calcul (rapports calculés dans la même cellule ; les temps absolus varient avec la machine) — le routage top-k a un coût propre qui mange une partie de l’économie théorique ; qualité : la tâche jouet ne discrimine pas (acc 1,000 partout, dit comme tel) — l’information est dans les routeurs : sans loss d’équilibrage, parts 0,284…0,000 ; avec α=0,01, parts 0,118–0,134 quasi uniformes (L_aux 1,29 → 1,00) ; 3 exercices
3.4d-Fourier-Features-Biais-Spectral Le biais spectral d’un MLP tanh reproduit empiriquement (Rahaman et al. 2019) face à un signal 1D à 13 cycles, puis débloqué par le mapping de Fourier γ(x) = [cos(2πB x), sin(2πB x)] avec B ~ N(0, σ²) (Tancik et al. 2020, geste NeRF) — trois régimes mesurés (sous-ajustement, bande utile, aliasing) plus un témoin négatif qui combine σ=50 et h=256 : la capacité mémorise le train sans récupérer le test Le MLP n’apprend pas les hautes fréquences : la NTK tanh a une bande passante trop étroite pour la cible 13 cycles ; un mapping B ~ N(0, σ²) adapté la débloque, σ excessif re-sur-ajuste — la MSE comme bande à dépasser, pas comme optimum ponctuel garde gradient 2,08e-07 (seuil 1e-5) ; balayage σ (64 pts train / 400 test, seed 0) : baseline 3,23e-01 (plafond spectral), σ=1 1,40e-02, σ=2 minimum à 8,33e-03, σ=5 1,21e-02, σ=10 9,70e-03, σ=20 7,41e-02 (aliasing), σ=50 4,42e-01 ; témoin négatif σ=50 + h=256 (99 073 paramètres) : MSE train 1,36e-07 contre test 4,32e-01, rapport 3,2e-07 — la capacité mémorise le train sans gain au test ; 3 exercices
3.5-Phenomenes-de-Generalisation Grokking et double descente reproduits en NumPy pur (MLP à embeddings + Adam à la main), confrontés à la borne PAC du 2.8 Le phénomène sans la boîte noire : mémorisation → transition abrupte, et le W de la double descente garde gradient ≤ 1e-6 (embeddings inclus) ; grok mesuré : train saturé ~500 pas, test 100 % des dizaines de milliers de pas plus tard (wd = 1) ; contre-témoin wd = 0 ; double descente : pic au seuil M ≈ n (×5 le creux), asymptote moderne sous le creux classique, 20 graines
3.6-Modeles-Generatifs VAE, GAN et diffusion (DDPM) écrits en NumPy pur, même cible (huit modes sur un cercle), même budget (6 000 pas, batch 256, largeur 64), face à une baseline GMM ajustée par EM Trois objectifs, trois échecs : le VAE couvre mais moyenne, le GAN s’effondre, la diffusion raffine au prix de 100 passes par échantillon garde gradient 1,4e-08 (ELBO + reparamétrisation) et 3,2e-10 (eps-net) ; multi-4-graines : GMM 8/8 disp 0,99 (2,2 s) ; VAE 8/8 disp 6,0 ; GAN 1,5 ± 0,5 modes ; DDPM 8/8 disp 1,51 ; trajectoire de denoising en 5 instantanés (t = 99 → 0)
3.6b-Modeles-Generatifs-PyTorch Versant framework du 3.6 : VAE, GAN et DDPM (diffusion) entraînés sur une même cible 2D à 4 modes (mélange de Gaussiennes, PyTorch CPU) puis comparés — ELBO vs adversarial vs débruitage Le compromis qualité/diversité : 4 mécanismes génératifs sur les mêmes métriques (couverture de modes, entropie effective), le GMM empirique en référence « modèle exact » médiane sur 3 graines (budget de pas commun) : GMM cov 4/4 ESS 3,96 · VAE cov 4/4 ESS 3,95 · GAN cov 3/4 ESS 2,99 (perte de diversité, un mode jamais couvert) · Diff cov 4/4 ESS 1,62 (couvre mais sous-échantillonne le mode le plus faible) — verdict nuancé, pas de « gagnant » unique
3.6c-Modeles-Generatifs-Diffusion-from-scratch Le DDPM écrit à la main : schedule β, forme fermée du forward, réseau ε_θ, loss MSE sur le bruit, sampler ancestral — sans librairie de diffusion La diffusion sans boîte noire : chaque pièce du DDPM démontrée et mesurée, schedules linéaire et cosinus comparés à budget égal 16 entraînements + échantillonnages (17,7 min CPU) ; MMD médiane sur 4 graines par budget × bande passante : le cosinus domine aux petites σ (budget 24, σ 0,25 : 0,00197), le linéaire reprend aux grandes (budget 12, σ 2,0 : 0,02087 vs 0,02359)
3.6d-Modeles-Generatifs-Score-SDE-from-scratch Le passage à la limite continue du DDPM : réseau de score, formulation SDE, et trois solveurs (Euler-Maruyama, Langevin recuit, prédicteur-correcteur) écrits à la main La vue continue de la diffusion : mêmes cibles, plusieurs solveurs, tous mesurés contre un plancher vrai/vrai plancher vrai/vrai MMD 1,6e-4 ; Euler-Maruyama (réseau) 0,00156, modes 5/5, 1,0 s ; prédicteur-correcteur 0,00259 ; Langevin recuit 0,00894 (12,6 s) ; contre-exemple « mauvais signe » : 0,680, modes 1/5
3.6e-Modeles-Generatifs-Conditionnels-from-scratch Génération conditionnelle et classifier-free guidance from scratch : le DDPM conditionné par classe, balayage du poids de guidance w Guider sans classifieur : le compromis adhérence/diversité du CFG mesuré sur le même générateur meilleur w = 1,0 sur les trois bandes passantes (MMD 0,009 à 0,029) ; w = 5,0 : adhérence 0,991 mais la diversité s’effondre (MMD jusqu’à 0,27)
3.7-Distillation-Maitre-Eleve Distillation teacher/student : un maître entraîné distille son savoir (dark knowledge) vers un élève ~9× plus petit Le facteur T² vérifié : la KL brute chute en ~1/T², la KL scalée reste constante ; verdict INCONCLUSIVE au seuil strict — gain par exemple net (DM p < 0.001, CE 0.63 → 0.58) mais edge 0.6σ/1.4σ sous 5 folds × 4 graines maître 0.8906 / distillé 0.8145 vs baseline 0.8029 ; ECE 0.0458 vs 0.0684 ; ratio params 8.9× ; 5 folds × 4 graines (20 paires), entrainement deterministe
3.8-Representations-Contrastives Pré-entraînement contrastif moderne sur vues continues : augmentations contrôlées du sac-de-mots (mask/swap/identité), encodeur MLP et loss InfoNCE écrits from scratch sans autograd, pont explicite vers le skip-gram (cooccurrence discrète vs vue continue) Apprendre des représentations sans étiquettes : deux vues d’une même phrase attirent leurs embeddings, les autres phrases les repoussent sonde linéaire 0,432 (chance 1/7 = 0,143 ; aléatoire gelé 0,161 ; skip-gram BoW 0,154 ; supervisé from scratch 0,368) ; contre-témoin de collapse mesuré (verdict NON) ; ablations température × augmentations × graines avec écart-type inter-graines ; 3 exercices
3.9-Compression-Quantization-FP La quantification flottante FP32 → FP16/BF16 écrite bit à bit (arrondi, troncature, découpe mantisse/exposant), vérifiée contre torch.half() et torch.bfloat16 Le geste fondateur de la compression : réduire la largeur d’un format soi-même et mesurer exactement ce qu’on perd arrondi maison = torch.half()/bfloat16 exactement (0 écart sur 20 000 valeurs) ; troncature = facteur 2,0 en erreur moyenne (FP16 2,009, BF16 1,981) ; erreurs sous les bornes théoriques (FP16 arrondi 4,85e-4 ≤ 4,88e-4) ; falaise FP16 à 65 520 (inf), BF16 la franchit (9,97e37)
3.9a-Compression-Quantization-INT8 La quantification INT8 construite à la main (mapping affine, fake-quant per-tensor/per-channel, activations dynamiques par hooks, calibrations statiques min/max et KL — port fidèle TensorRT) sur ResNet-20/CIFAR-10 entraîné dans le notebook, puis la falaise INT4 Le déjeuner gratuit et sa limite : INT8 égale le FP32 à ±0,001 près pour 4× moins de mémoire ; la discrimination vit dans l’erreur de poids et la falaise INT4 FP32 0,9019 ; w8 per-tensor 0,9024 / per-channel 0,9020 (erreur s3.1.conv1 : 1,05e-2 vs 7,72e-3) ; dynamique w-channel 0,9018 ; statique min/max 0,9017 ; statique KL 0,9012 (seuils 60-100 % du range, masse coupée ≤ 0,005 %) ; INT4 0,8778 (−2,4 pts, erreur ×18) ; 270 906 poids : 1,08 Mo → 0,27 Mo (4,0×)
3.9b-Compression-Pruning-from-scratch L’élagage construit à la main : masque de magnitude (kthvalue) par couche et global, le piège « sparse n’est pas smaller » (octets et MACs du tenseur dense inchangés), puis élagage structuré — filtres entiers, transfert de poids sous la contrainte de couplage résiduel, contrôle d’identité bit-à-bit, recalibrage BatchNorm — et la loterie (LTH), sur le même ResNet-20/CIFAR-10 que 3.9a Contrepoint §9 : l’outil standard torch.nn.utils.prune (l1_unstructured) sur le même réseau, prune.remove pour replier le masque ; comparatif quatre voies à 90 % gardés (§10) : dense, élagage simple, LTH, outil standard. Sparse n’est pas smaller : la sparsité est une propriété des valeurs, la taille une propriété des formes ; seuls les filtres entiers coupés réduisent réellement poids et MACs Référence 0,8980 ; à 80 % gardés sans réentraînement : 0,8956 (par couche) / 0,8964 (global) ; 10 % de zéros : 270 896 valeurs stockées (1058,2 Kio) et 40,81 M MACs inchangés ; structuré keep=0,50 : 80 762 poids (−70,4 %) et 21,48 M MACs (−47,4 %), transfert brut 0,1001 → recalibré BN 0,1001 → réentraîné 0,8447, init aléatoire à largeur égale 0,7115 ; keep=0,75 réentraîné 0,8906 ; LTH 90 % gardés reset+réentraîné 0,8172 ; contrôle d’identité à keep=1,0 : écart max 0 | Contrepoint §9 : accord des masques manuel/outil 100 % (couche stem ; sparsités 9,999 % / 10,000 %), coupe sans réentraînement 0,8970 ; §10 quatre voies : dense 0,8980 / élagage simple 0,8994 / outil standard 0,9021 / LTH 0,8172.
3.9c-Pruning-From-Scratch Trois familles de pruning écrites en numpy pur — unstructured magnitude top-k% par layer (Han et al. 2015), structured filter pruning L1-norm sur kernels (Li et al. 2017), Lottery Ticket Hypothesis reset-poids-init + retrain (Frankle & Carlin 2019) — puis confrontation avec torch.nn.utils.prune (Bloc B.2 #16060) sur MLP 784-256-128-10 / MNIST subset 20k (CPU-compatible, <10 min) Le reset à l’init est la clef LTH — sur terrain léger, Pruning simple gagne : à 80 % de sparsité effective (mask reappliqué après opt.step(), Tell c.1166), LTH 0.9678 et Pruning simple 0.9704 ; torch.nn.utils.prune 0.9719 reste au-dessus (hook forward_pre_hook natif) ; l’avantage LTH documenté (Frankle & Carlin 2019) joue sur modèles plus grands (ResNet/CIFAR-10, voir 3.9b) Dense 0.9675, torch.nn.utils.prune 0.9719, LTH 0.9678, Pruning simple 0.9704 ; 235 146 paramètres, ~7s/epoch CPU, 5 epochs/méthode ; 3 exercices C.1
3.9d-Compression-Distillation-from-scratch La distillation de Hinton écrite à la main (loss (1−α)·CE + α·T²·KL sur softmax tempérées, hint FitNets en MSE relative) : un maître ResNet-20 (272 474 paramètres) enseigne à un élève ResNet-8 (78 042, ratio 3,5×) sur CIFAR-10, même recette SGD/cosine/crop+flip, 3 graines + balayage T ∈ {1, 2, 4, 8} Le rattrapage partiel mesuré en σ : à budget égal, l’élève distillé gagne +2,05 pts (+12,6 σ sur 3 graines) — le pendant « entraînement » de la compression, là où le 3.9a montre son pendant « déploiement » maître 0,9012 ; scratch 0,8437 ± 0,0015 ; KD T=4 α=0,7 0,8642 ± 0,0006 (+12,6 σ, écart-type ÷ 2,5) ; + hint FitNets 0,8654 ± 0,0022 (~0,5 σ vs KD seul — verdict honnête) ; balayage T 0,8512 / 0,8624 / 0,8647 / 0,8595 (sommet mesuré T=4) ; dark knowledge : masse non-cible 0,086 → 0,609 entre T=1 et T=4 ; 3 exercices
3.9e-Compression-Quantization-SOTA Le pendant SOTA de 3.9a : la même INT8 par l’écosystème torch.ao — quantification dynamique, FX graphes statiques (MinMax, Histogramme), observateurs déclarés L’écosystème contre la main : même exactitude à ±0,001, zéro mécanique écrite témoin FP32 0,7896 (recette CPU 6 époques) ; dynamique fc INT8 0,7898 (+0,0002) ; FX MinMax 0,7900 (+0,0004, 10 conv fusionnés) ; FX Histogramme 0,7901 (+0,0005)
3.9f-Compression-Pruning-SOTA Le pendant élagage de la famille compression : magnitude pruning écrit à la main (~10 lignes) puis déclaré à l’API torch.nn.utils.prune (l1_unstructured, global_unstructured, random_unstructured en contre-témoin, ln_structured par canaux), sur le même témoin ResNet-20/CIFAR-10 Un masque, quatre décisions : critère, allocation locale/globale, granularité, persistance — l’API garantit le masque (hooks), la main donne le contrôle témoin FP32 0,7896 (recette CPU 6 époques) ; main 50 % : 0,5775 ≈ API L1 50 % : 0,5791 (même sélection, écart 0,0016 = ordres flottants) ; global L1 75 % : 0,4031 vs random 75 % : 0,1000 — l’allocation bat l’uniforme à budget égal ; structuré 50 % canaux : 0,1000 (336 canaux morts) ; state_dict 1071 Ko → 2129 Ko (orig+masque) → 1071 Ko après remove() ; latence 30,7 → 33,8 ms : rien gagné sans format épars
3.9g-Compression-Comparatif-A-vs-B Le capstone comparatif de la famille compression (bloc 7 de #16060) : le même ResNet-20 entraîné une fois (40 époques GPU), passé sous chaque méthode avec les mêmes instruments — accuracy, taille brute + compressée zlib, latence CPU médiane batch 64, FLOPs par hooks, LOC par inspect.getsource — INT8 dynamique manuel (per-tensor uint8) vs quantize_dynamic, pruning magnitude 50 % post-training par couche (manuel, kthvalue) vs global_unstructured (sélection globale L1), FP16 La vraie différence A-vs-B n’est pas l’outillage, c’est la politique : INT8 dynamique sur un CNN à dominance conv ne touche que 0,2 % des poids (±0,02 pt, aucune accélération — le gain INT8 des CNN vit dans le FX statique de 3.9e) ; à budget de coupes égal, l’allocation globale conserve 5,96 pts de plus que la par couche FP32 0,9036 ; INT8 manuel 0,9038 (+0,02, 25 LOC) ≈ torch.ao 0,9036 (0,00, 1 LOC) ; pruning par couche 0,8122 (−9,14, 14 LOC) vs global 0,8718 (−3,18, 1 LOC) ; tailles brutes 1070,6 Ko partout sauf FP16 535,4 Ko (×2,0, 0,9038) ; zlib 1003,6 → 597,2/594,9 Ko pour le pruning (×1,7) ; latence CPU 25,7-30,3 ms sans gain structurel pour aucune méthode ; FLOPs 40,8 M partout ; 3 exercices C.1 (per-channel, taux de coupure par dichotomie, pipeline combiné)
3.10-Modeles-Generatifs-Diffusion-SOTA Le pendant SOTA de 3.6c/d/e : la bibliothèque diffusers (DDPM, DDIM, UNet outillé, pipelines) face aux implémentations maison La librairie : moins de code, plus d’outils, mêmes maths — et la stochasticité de η rendue visible maison ancestral 31 LOC, 92,7 ms/img, MMD 0,0419 ; maison DDIM(20) 22 LOC, 1,6 ms/img ; lib DDIM(20) 8 LOC, 7,0 ms/img, 0,0448 ; classes couvertes 10/10 (maison) vs 9/10 (lib) ; η=1,0 : écart 0,34 entre 2 runs de même x_T
3.11-Budget-Memoire-Entrainement Le budget mémoire d’un entraînement — poids, gradients, états d’optimiseur, activations — calculé à la main puis confronté à la mesure sur un mini-GPT réel, chaque écart servant à corriger le modèle (premier volet de #18210) Les activations ne sont pas proportionnelles au lot : la rétropropagation retient les matrices engagées dans les multiplications, donc la loi est affine, A = A₀ + k·lot — et le terme constant A₀ vaut, à 0,87 % près, les poids eux-mêmes formule des paramètres corrigée par la mesure : les termes omis (plongement positionnel, LayerNorms) valaient +4 736/+10 496/+17 280 selon la taille, résidu 0 après correction et décomposé terme à terme (12 288 + 4 608 + 384) ; ajustement affine sur lots 1-32 : A₀ = 3 667 524 o, k = 2 815 488 o/lot, résidus nuls, A₀/P = 0,9913, coefficient c = 21,48 valeurs retenues par élément (littérature en précision mixte : 34, rapport 0,63) ; le rapport naïf A/(lot·s·d·L) surestime c à petit lot (49,46 au lot 1 → 22,35 au lot 32) ; lois vérifiées en s (écart ≤ 0,1 %) et en d (A₀ ×3,50/×3,72, k ×1,69/×1,81 — sous-linéaire, dit comme tel) ; budget à lot 8 : petit 8,36 GiB, moyen 56,68 GiB, grand 209,84 GiB, lot maximal 24 GiB → 29 / 1 / ne tient pas ; mesure sur RTX 3090 : après modèle +22,17 MiB contre 22,15 attendus, et pic de rétropropagation 346,56 MiB inférieur à la somme analytique 368,52 (rapport 0,940) ; 3 exercices C.1
3.12-Les-Collectives L’anneau all-reduce écrit à la main (reduce-scatter puis all-gather, batch_isend_irecv) contre l’appel natif dist.all_reduce, sur gloo/CPU à 2 et 4 rangs — exactitude, éléments sur le fil, messages, puis chronométrage (deuxième volet de #18210) Le compte, pas la durée : le volume par rang de l’anneau, 2(p−1)n/p, tend vers 2n quand p grandit — la moitié du schéma naïf à 4 rangs — mais la somme flottante n’étant pas associative, les deux implémentations ne rendent pas le même résultat égalité au bit avec l’appel natif sur des entiers ; à 4 rangs 12 messages (= 4(p−1)) et 18 éléments envoyés par rang contre 36 pour le schéma naïf (exactement p/2), à 2 rangs 12 contre 12 (aucun gain) ; la non-associativité est d’abord établie hors réseau — ((1e16+1,0)−1e16)+3,0 = 3,0 contre (1e16−1e16)+(1,0+3,0) = 4,0 — puis retrouvée dans la mesure : à 4 rangs l’anneau rend 3,0 et le natif 4,0 (écart 1,0), à 2 rangs les deux coïncident (1e16, le petit terme absorbé) ; chronométrage : à 4 rangs et 2 Mo l’anneau est plus lent sans recouvrement (facteur 1,3-1,6 selon la série), à 32 Mo les deux plages se recouvrent (inséparables, dit comme tel) ; inversion mesurée que le carnet n’explique pas — à 32 Mo le natif à 2 rangs (0,102-0,284 s) est ~1,8× plus lent qu’à 4 rangs (0,068-0,119 s) ; noyau coursia-ml-training requis (gloo échoue à créer son périphérique sous le noyau générique — mesuré) ; 3 exercices C.1
3.13-Decouper-le-Modele-DDP-ZeRO-FSDP Où va la mémoire quand on découpe : DistributedDataParallel, ZeroRedundancyOptimizer (ZeRO-1) et FullyShardedDataParallel (ZeRO-2 SHARD_GRAD_OP et ZeRO-3 FULL_SHARD) mesurés sur le même modèle à 2 et 4 rangs, puis l’occupation du parallélisme de pipeline — GPipe contre 1F1B — comptée par saved_tensors_hooks (troisième volet de #18210) Ce qui est découpé décide de tout, et deux stratégies peuvent coïncider au repos : ZeRO-2 et ZeRO-3 rendent le même total à l’arrêt, parce que la différence documentée entre elles est quand FSDP resharde les paramètres — donc dans le pic, invisible sur un relevé au repos, et dit comme tel par rang sur 526 336 paramètres (4P = 2 105 344 o) : DDP 8 421 440 o (4,00 P) à 2 comme à 4 rangs — répliqué, insensible au monde ; ZeRO-1 6 316 064 o (3,00 P) puis 5 263 376 o (2,50 P), seul l’état d’optimiseur (8P) se divisant ; ZeRO-2 et ZeRO-3 identiques, 4 210 692 o (2,00 P) puis 2 105 348 o (1,00 P) ; deux pièges de mesure documentés — l’état de ZeRO-1 vit dans l’optimiseur local (opt.optim.state), une lecture sur l’enveloppe rend 0 et fabrique une économie totale qui n’existe pas, et FSDP refuse le CPU dans sa seule branche d’auto-détection, qu’un device_id=torch.device("cpu") explicite contourne légitimement ; pipeline à 2 rangs : pic de tenseurs vivants du dernier étage 5 × horizon pour GPipe (10 / 20 / 40 aux horizons 2 / 4 / 8) contre constant à 5 pour 1F1B, pour 40 sauvegardes dans les deux cas — et mb_args attend un micro-lot, le lot complet faisant bloquer gloo sans message ; 3 exercices C.1
3.13b-Deux-Cartes-Reelles-DDP-FSDP-nccl Le même dispositif que 3.13 transporté sur deux vraies cartes hétérogènes (RTX 3090 et RTX 3080 Ti) sous nccl, dans un conteneur Linux : DDP puis FSDP FULL_SHARD à 2 rangs, un par carte — le pic CUDA mesuré (max_memory_allocated, pas recalculé) et le temps par pas s’ajoutent aux octets détenus, puis le diagnostic du rang lent : une pause de 50 ms injectée après chaque pas sur un seul rang (quatrième volet de #18210, sous #19256) Le découpage paie en mémoire et facture en temps — et dans un couple déséquilibré, celui qui attend n’est pas celui qui traîne : le sharding divise l’état détenu mais rallonge le pas, car chaque couche se regroupe à la volée ; et l’attente du all-reduce se lit chez la carte saine, pas chez la lente par rang sur 134 M paramètres : DDP détient 4,00 P et pointe à 3 091 Mo, FSDP retombe à 2,00 P et pointe à 1 299 Mo — au-dessus du 2P nu (1 074 Mo) : les tampons de regroupement vivent par-dessus l’état découpé ; pas médian 402 ms (DDP) contre 654 ms (FSDP), soit environ 63 % de temps par pas en plus pour un pic réduit d’un facteur 2,4 — un arbitrage, pas un gain gratuit ; rang lent : la pause de 50 ms sur le rang 0 laisse ses propres pas courts (394 ms) et fait passer le rang sain de 402 à 444 ms — il paie exactement la pause de l’autre ; exécution via conteneur pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime (--gpus all, --shm-size 8g) ; 3 exercices C.1

Feuille de route

Les deux chantiers annoncés par cette feuille de route sont livrés : théorie de l’information appliquée — entropie, KL, cross-entropy (#12420, livrée par #12640 → notebook 3.0) ; régularisation — dropout, weight decay, early stopping (#12409, livrée par #12527 → notebook 3.3). Le fil directeur n’a pas changé et vaut pour la suite : chaque mécanisme écrit à la main, vérifié contre torch, puis consommé via l’API officielle.

Bloc A — Compression de modèles from scratch (EPIC #16060, sous-notebooks par technique) : - Bloc A.1 — Quantification FP32→FP16 [3.9] (#16127, OPEN po-2026) - Bloc A.2 — Quantification INT8 [3.9a] (#16094, MERGED po-2023) - Bloc A.3 — Pruning from scratch [3.9c] (#16060, PR en cours po-2024 — unstructured magnitude, structured filter pruning, Lottery Ticket Hypothesis) - Bloc A.4 — Distillation teacher/student [3.7] (#12675, MERGED — teacher/student sur MLP)

Bloc B — Confrontation au SOTA (mêmes techniques, via les libs standard) : - Bloc B.2 — torch.ao.quantization [3.9e] / torch.nn.utils.prune [3.9f] (livrés). - Bloc 7 — Comparatif capstone A vs B [3.9g] : le même ResNet-20 sous chaque méthode, mêmes instruments (accuracy, taille brute/zlib, latence CPU, FLOPs, LOC) — ferme l’EPIC #16060.

Bloc C — Entraînement distribué (#18210, livré — prolongé par #19256) — ce qu’un pas d’entraînement coûte quand le modèle ne tient plus sur une machine : - Volet 1 — Budget mémoire d’un entraînement [3.11] (#18210) : les quatre postes, calculés puis mesurés, et la loi affine des activations. - Volet 2 — Les collectives [3.12] (#18210) : l’anneau all-reduce à la main contre l’appel natif, comptes sur le fil à l’appui. - Volet 3 — DDP / ZeRO / FSDP et parallélisme de pipeline (GPipe contre 1F1B) [3.13] (#18210) : la mémoire par rang selon la stratégie de découpage, et l’occupation en vol des activations selon l’ordre des micro-lots. - Volet 4 — Deux vraies cartes : DDP et FSDP sous nccl, et le rang qui fait attendre l’autre [3.13b] (#19256) : le pic CUDA et le temps par pas mesurés sur cartes hétérogènes, et la signature du déséquilibre — l’attente se lit chez la carte saine.

Prérequis

Environnement

pip install numpy matplotlib
# torch : parité en 3.1-3.4, moteur principal en 3.6b, 3.7, 3.9-3.9g, 3.10 à 3.13
# (kernel coursia-ml-training pour 3.7, 3.9d, 3.12 et 3.13) ; 3.10 exige en plus diffusers :
# 3.12 utilise torch.distributed (gloo, CPU) : rien à installer de plus, mais le
# noyau doit être un torch dont gloo sait créer son périphérique — le noyau
# générique échoue là-dessus (le carnet le documente, la mesure à l'appui).
# 3.13 ajoute torch.distributed.fsdp et torch.distributed.pipelining, livrés avec
# torch : rien à installer non plus.
pip install torch torchvision
pip install diffusers

Tous les notebooks tournent sur CPU — la plupart en moins de dix minutes. Exceptions : la famille compression (3.9a, 3.9b, 3.9e, 3.9f, 3.9g) entraîne ResNet-20 sur CIFAR-10 — la recette complète (40 époques, ~8 min sur RTX 3090 ; ~30 min pour 3.9b, bras de réentraînement compris) exige un GPU ; sur CPU ces notebooks tournent une recette réduite de 6 époques (3 pour les bras de 3.9b ; ~9-15 min), le témoin GPU de 3.9a (0,9019) servant de référence croisée. 3.9d exige aussi un GPU pour la distillation complète (maître + 9 entraînements d’élève + balayage T, ~2 h sur RTX 3080 Ti Laptop) ; sur CPU, recette réduite (6 époques, 1 graine). Et 3.6c demande ~19 min CPU pour ses 16 entraînements.

Retour au sommet