Accélération GPU / CUDA PyTorch

Les opérations fondamentales de l'apprentissage profond sont la multiplication matricielle à grande échelle et les opérations élément par élément. Les CPU sont conçus pour traiter une logique série complexe, généralement avec 8 à 64 cœurs ; les GPU, en revanche, possèdent des milliers de cœurs parallèles simples, ce qui les rend naturellement adaptés à ce type de calcul numérique hautement parallèle. PyTorch, grâce auCUDA(Compute Unified Device Architecture) framework, fait appel au GPU, ce qui peut accélérer l'entraînement de dizaines voire de centaines de fois.


1. Différences entre CPU et GPU

L'amélioration de la vitesse d'entraînement sur GPU vient principalement de deux aspects : premièrement, l'exécution parallèle d'un grand nombre de calculs identiques ; deuxièmement, la mémoire GPU à haut débit permet des transferts de données bien plus rapides que la mémoire CPU.

Pour les opérations intensives en calcul telles que la multiplication matricielle, l'effet d'accélération est particulièrement important.

Élément de comparaison CPU GPU(NVIDIA)
Nombre de cœurs 8-64 grands cœurs Des milliers de petits cœurs (CUDA Cores)
Objectif de conception Traitement série à faible latence Calcul parallèle à haut débit
Bande passante mémoire ~50~100 GB/s ~500~3000 GB/s
Vitesse de multiplication matricielle Référence 10x-100x plus rapide
Interface PyTorch "cpu" "cuda"

Si vous utilisez Apple Silicon (M1/M2/M3), PyTorch, via lempsbackend, prend en charge l'accélération GPU Metal. L'utilisation est presque identique à CUDA ; changez simplement le device pour"mps"。


2. Détection de l'environnement CUDA

Avant d'utiliser le GPU, vous devez vérifier si l'environnement actuel dispose d'un PyTorch compatible CUDA installé.

Vérifiez également si des périphériques GPU sont disponibles sur le système.

Exemple

import torch

# Si CUDA est pris en charge
print("CUDA disponible :", torch.cuda.is_available())

# Nombre de périphériques GPU
print("Nombre de GPU :", torch.cuda.device_count())

# Index du GPU par défaut actuel
print("GPU actuel :", torch.cuda.current_device())

# Nom du modèle de GPU
print("Modèle de GPU :", torch.cuda.get_device_name(0))

# Version de PyTorch et version de CUDA avec laquelle il a été compilé
print("Version de PyTorch :", torch.__version__)
print("Version de CUDA :", torch.version.cuda)

Exemple de sortie :

CUDA 可用: True
GPU 数量: 1
当前 GPU: 0
GPU 型号: NVIDIA GeForce RTX 4090
PyTorch 版本: 2.3.0+cu121
CUDA 版本: 12.1

2.1 Sélection dynamique d'un périphérique (approche recommandée)

Codage en dur dans le code"cuda"Cela provoquera directement une erreur sur les machines sans GPU.

Exemple

import torch

# Méthode 1 : Approche classique, meilleure compatibilité
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Méthode 2 : Recommandée pour PyTorch 2.0+, prend automatiquement en charge CUDA / MPS / CPU
device = (
    "cuda" if torch.cuda.is_available()
    else "mps" if torch.backends.mps.is_available()
    else "cpu"
)

print(f"Using device: {device}")

3. Déplacer des tenseurs entre les périphériques

Dans PyTorch, les tenseurs sont créés sur le CPU par défaut.

Pour utiliser le calcul GPU, vous devez explicitement déplacer les tenseurs vers le GPU, ou les créer directement sur le GPU.

3.1 Méthodes de déplacement de base

Exemple

import torch

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Créer un tenseur CPU
cpu_tensor = torch.tensor([1.0, 2.0, 3.0])
print(cpu_tensor.device)   # cpu

# Méthode 1 : .to(device) — recommandée, la plus polyvalente
gpu_tensor = cpu_tensor.to(device)

# Méthode 2 : .cuda() — environnement CUDA uniquement
gpu_tensor = cpu_tensor.cuda()

# Méthode 3 : Spécifier l'appareil directement lors de la création
gpu_tensor = torch.tensor([1.0, 2.0, 3.0], device=device)
gpu_tensor = torch.randn(3, 4, device=device)

# Revenir au CPU (pour l'affichage, la conversion numpy, la sauvegarde, etc.)
back_to_cpu = gpu_tensor.cpu()

print(gpu_tensor.device)    # cuda:0
print(back_to_cpu.device)   # cpu

Lors de la conversion d'un tenseur GPU en numpy, vous devez d'abord le ramener sur le CPU, et si le tenseur a des gradients, vous devez également le détacher d'abord :

Exemple

# Convertir un tenseur GPU ordinaire en numpy
arr = gpu_tensor.cpu().numpy()

# Convertir un tenseur GPU avec gradients en numpy
arr = gpu_tensor.detach().cpu().numpy()

3.2 Contrainte de cohérence des périphériques

Les tenseurs sur différents périphériques ne peuvent pas participer directement à la même opération.

Sinon, cela lèvera une erreurRuntimeError:

Exemple

a = torch.randn(3).to("cuda")
b = torch.randn(3)           # Sur CPU

# c = a + b # RuntimeError: Tous les tenseurs doivent être sur le même périphérique

# Approche correcte : unifier d'abord le périphérique
c = a + b.to("cuda")

Vérifiez l'appareil sur lequel se trouve le tenseur :

Exemple

x = torch.randn(3, 4).to(device)

print(x.device)           # cuda:0
print(x.is_cuda)          # True
print(x.get_device())     # 0 (indice GPU)

3.3 Vérification de la comparaison de vitesse

Exemple

import torch
import time

device = torch.device("cuda")
n = 5000

# Multiplication matricielle CPU
a_cpu = torch.randn(n, n)
b_cpu = torch.randn(n, n)
start = time.time()
c_cpu = torch.matmul(a_cpu, b_cpu)
print(f"Temps CPU : {time.time() - start:.3f}s")

# Multiplication matricielle GPU
a_gpu = a_cpu.to(device)
b_gpu = b_cpu.to(device)
torch.cuda.synchronize()    # Assurez-vous que le transfert de données est terminé avant de démarrer le chronomètre
start = time.time()
c_gpu = torch.matmul(a_gpu, b_gpu)
torch.cuda.synchronize()    # Attendez que le GPU termine l'exécution avant d'arrêter le chronomètre
print(f"Temps GPU : {time.time() - start:.3f}s")

Exemple de sortie :

CPU 耗时: 1.847s
GPU 耗时: 0.021s

Le calcul GPU estexécuté de manière asynchrone—après le retour de l'appel Python, l'opération GPU peut ne pas être terminée. Lors de la mesure du temps, vous devez appelertorch.cuda.synchronize()pour attendre que le GPU ait réellement terminé ; sinon, les résultats de mesure sont inexacts.


4. Déplacement du modèle vers le GPU

Tous les paramètres du modèle (weight、bias) sont essentiellement des tenseurs.

Ces paramètres doivent également être déplacés vers le GPU afin d'effectuer la propagation avant et la rétropropagation sur le GPU.

Appel.to(device)sur l'ensemble du modèle, et PyTorch parcourra et déplacera automatiquement tous les paramètres internes.

Exemple

import torch
import torch.nn as nn

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(784, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Linear(128, 10),
        )

    def forward(self, x):
        return self.net(x)

# Déplacer le modèle vers le GPU, il suffit de l'appeler une fois
model = SimpleNet().to(device)

# Vérifier que tous les paramètres sont sur le GPU
for name, param in model.named_parameters():
    print(f"{name}: {param.device}")
# net.0.weight: cuda:0
# net.0.bias: cuda:0
# ...

# Les données d'entrée doivent également être sur le même périphérique
x = torch.randn(32, 784).to(device)
output = model(x)
print(output.shape)   # torch.Size([32, 10])

Lorsque le modèle est sur le GPU mais que les données d'entrée sont encore sur le CPU, la propagation avant déclenchera une erreur. Veillez, après que DataLoader a lu chaque lot,inputsetlabelsà appeler .to(device) sur chacun d'eux..to(device)。


5. Flux de travail d'entraînement complet

Ce qui suit est un gabarit standard d'entraînement sur GPU qui comprend le chargement des données, l'entraînement du modèle et l'évaluation de validation.

Exemple

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# 1. Configuration du périphérique
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Périphérique d'entraînement : {device}")

# 2. Chargement des données
# pin_memory=True : Verrouille les données en mémoire pour accélérer le transfert CPU -> GPU
# num_workers : Préchargement multiprocessus pour réduire le temps d'attente des données
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,)),
])
train_dataset = datasets.MNIST(root="./data", train=True,
                                download=True, transform=transform)
test_dataset  = datasets.MNIST(root="./data", train=False,
                                download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=256, shuffle=True,
                          num_workers=4, pin_memory=True)
test_loader  = DataLoader(test_dataset,  batch_size=256, shuffle=False,
                          num_workers=4, pin_memory=True)

# 3. Définir le modèle
class CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(64 * 7 * 7, 256), nn.ReLU(), nn.Dropout(0.5),
            nn.Linear(256, 10),
        )

    def forward(self, x):
        return self.classifier(self.features(x))

model     = CNN().to(device)     # Déplacer le modèle vers le GPU
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)

# 4. Fonction d'entraînement
def train_epoch(model, loader, optimizer, criterion):
    model.train()
    total_loss, correct = 0.0, 0
    for inputs, labels in loader:
        # non_blocking=True : transfert asynchrone, le CPU peut continuer à préparer le lot de données suivant
        inputs = inputs.to(device, non_blocking=True)
        labels = labels.to(device, non_blocking=True)

        optimizer.zero_grad()
        outputs = model(inputs)
        loss    = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        total_loss += loss.item() * inputs.size(0)
        correct += (outputs.argmax(1) == labels).sum().item()

    n = len(loader.dataset)
    return total_loss / n, correct / n

# 5. Fonction de validation
def eval_epoch(model, loader, criterion):
    model.eval()
    total_loss, correct = 0.0, 0
    with torch.no_grad():
        for inputs, labels in loader:
            inputs = inputs.to(device, non_blocking=True)
            labels = labels.to(device, non_blocking=True)
            outputs = model(inputs)
            loss    = criterion(outputs, labels)
            total_loss += loss.item() * inputs.size(0)
            correct += (outputs.argmax(1) == labels).sum().item()
    n = len(loader.dataset)
    return total_loss / n, correct / n

# 6. Boucle d'entraînement principale
for epoch in range(1, 11):
    train_loss, train_acc = train_epoch(model, train_loader, optimizer, criterion)
    val_loss,   val_acc   = eval_epoch(model,  test_loader,  criterion)
    print(f"Époque {epoch:02d} | "
          f"Perte d'entraînement : {train_loss:.4f}, Précision : {train_acc:.4f} | "
          f"Perte de validation : {val_loss:.4f}, Précision : {val_acc:.4f}")

Points clés :

  • deviceLes variables sont déclarées tout en haut du code, référencées globalement, jamais codées en dur"cuda"
  • Modèle.to(device)ne doit être appelé qu'une seule fois
  • Pour chaque batch,inputsetlabelsdoit également être.to(device)
  • Paramètres de DataLoaderpin_memory=Trueetnon_blocking=Trueaccélérer le transfert de données
  • Utilisation en phase de validationtorch.no_grad()désactiver les gradients pour économiser de la mémoire

6. Entraînement multi-GPU

Lorsque la mémoire d'un seul GPU est insuffisante, envisagez d'utiliser l'entraînement parallèle multi-GPU.

De plus, le multi-GPU peut encore améliorer la vitesse d'entraînement. PyTorch propose deux approches principales.

6.1 DataParallel

C'est l'approche multi-GPU la plus simple.

Il utilise un seul processus, répartit chaque lot uniformément entre les GPU, exécute la propagation avant en parallèle et agrège les gradients sur le GPU principal pour les mises à jour. Il est facile à utiliser, mais le GPU principal est fortement chargé, l'utilisation entre les GPU est inégale, et il convient aux démarrages rapides.

Exemple

import torch
import torch.nn as nn

model = CNN()

if torch.cuda.device_count() > 1:
    print(f"Using {torch.cuda.device_count()} GPUs")
    model = nn.DataParallel(model)

    # Vous pouvez également spécifier quels GPU utiliser
    # model = nn.DataParallel(model, device_ids=[0, 1])

model = model.to("cuda")

# Le code d'entraînement suivant est exactement le même que pour un seul GPU
# DataParallel répartit automatiquement le batch uniformément entre les GPU et agrège les résultats

Si vous devez accéder aux attributs du modèle d'origine (tels que des méthodes personnalisées), vous devez.moduley accéder via :

Exemple

# Une fois model enveloppé par DataParallel, le modèle d'origine se trouve dans model.module
print(model.module.classifier)

# Lors de l'enregistrement du modèle, il est recommandé d'enregistrer model.module pour faciliter le chargement sur un seul GPU
torch.save(model.module.state_dict(), "model.pth")

6.2 DistributedDataParallel

Il s'agit de l'approche multi-processus.

Chaque processus est lié à un GPU, chaque carte détient une copie complète du modèle, et les gradients sont synchronisés via All-Reduce. C'est la solution recommandée pour les environnements de production et l'entraînement à grande échelle, avec une efficacité bien supérieure à celle de DataParallel.

Exemple

import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data.distributed import DistributedSampler

def main(rank, world_size):
    # Initialiser le groupe de processus ; le backend NCCL est optimisé pour les GPU NVIDIA
    dist.init_process_group(backend="nccl", rank=rank, world_size=world_size)

    # Lier chaque processus à son GPU correspondant
    torch.cuda.set_device(rank)
    device = torch.device(f"cuda:{rank}")

    # Envelopper avec DDP après avoir déplacé le modèle vers le GPU correspondant
    model = CNN().to(device)
    model = DDP(model, device_ids=[rank])

    # Utiliser DistributedSampler dans DataLoader pour s'assurer qu'aucune donnée ne se chevauche entre les GPU
    sampler = DistributedSampler(train_dataset,
                                  num_replicas=world_size, rank=rank)
    loader  = DataLoader(train_dataset, batch_size=64,
                         sampler=sampler, pin_memory=True)

    # La logique d'entraînement est identique à celle d'un seul GPU
    # ...

    dist.destroy_process_group()

# Comment lancer (recommandé : torchrun) :
# torchrun --nproc_per_node=4 train_ddp.py
Élément de comparaison DataParallel DistributedDataParallel
Nombre de processus Processus unique Multi-processus (un par GPU)
Backend de communication Limitation du GIL de Python NCCL (efficace)
Charge principale du GPU Lourd (agrégation de gradients) Équilibré (All-Reduce)
Modifications du code Minimal Modéré
Scénarios applicables Expérimentation rapide Entraînement en production

7. Entraînement en précision mixte AMP

L'entraînement utilise par défaut la précision float32 (FP32).

Précision mixte automatique (AMP)Utilise float16 (FP16) ou bfloat16 (BF16) pour certains calculs, offrant des avantages significatifs avec presque aucune perte de précision :

  • Utilisation de la mémoire réduite d'environ 50%
  • Vitesse d'entraînement améliorée de 2x–3x (accélération matérielle Tensor Core)
  • Très peu de modifications du code nécessaires ; seulement trois modifications

Exemple

from torch.cuda.amp import autocast, GradScaler

model     = CNN().to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
scaler    = GradScaler()    # Scaler de gradient FP16 pour éviter que les gradients ne subissent un underflow vers zéro

for epoch in range(num_epochs):
    model.train()
    for inputs, labels in train_loader:
        inputs = inputs.to(device, non_blocking=True)
        labels = labels.to(device, non_blocking=True)

        optimizer.zero_grad()

        # Modification 1 : sélectionner automatiquement FP16/FP32 dans la région autocast
        with autocast(device_type="cuda"):
            outputs = model(inputs)
            loss    = criterion(outputs, labels)

        # Modification 2 : mettre à l'échelle la perte avec scaler avant la rétropropagation
        scaler.scale(loss).backward()

        # Modification 3 : scaler met à jour les paramètres et gère automatiquement la mise à l'échelle des gradients en interne
        scaler.step(optimizer)
        scaler.update()

7.1 Choisir entre FP16 et BF16

Format Bits de précision Bits d'exposant Matériel compatible Caractéristiques
float16 10 bits 5 bits Séries RTX 20/30/40 Nécessite GradScaler pour éviter l'overflow
bfloat16 7 bits 8 bits A100 / H100 / RTX 4090 Même plage que FP32, entraînement plus stable

Si votre GPU prend en charge BF16, utilisez-le en priorité ; aucun GradScaler n'est nécessaire :

Exemple

# Utilisation de BF16 (PyTorch 2.0+)
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
    outputs = model(inputs)
    loss    = criterion(outputs, labels)

loss.backward()
optimizer.step()

8. Conseils d'optimisation des performances

Cette section présente plusieurs techniques pour optimiser l'utilisation de la mémoire et la vitesse d'entraînement.

8.1 Gestion de la mémoire GPU

Exemple

# Afficher l'utilisation actuelle de la mémoire
print(f"Alloué : {torch.cuda.memory_allocated() / 1024**2:.1f} MB")
print(f"En cache : {torch.cuda.memory_reserved() / 1024**2:.1f} MB")

# Afficher un rapport détaillé sur la mémoire
print(torch.cuda.memory_summary())

# Vider le pool de cache (ne libère pas la mémoire allouée)
torch.cuda.empty_cache()

# Utiliser inference_mode pendant l'inférence (plus rapide que no_grad, désactive complètement le moteur de gradient)
with torch.inference_mode():
    output = model(x)

# Gradient checkpointing : échanger du recalcul contre de la mémoire (couramment utilisé dans l'entraînement de grands modèles)
from torch.utils.checkpoint import checkpoint
output = checkpoint(model_block, x)

8.2 Optimisation de DataLoader

Exemple

loader = DataLoader(
    dataset,
    batch_size=256,
    num_workers=4,              # Préchargement multiprocessus ; recommandé de le définir à 50% des cœurs CPU
    pin_memory=True,            # Mémoire épinglée pour accélérer le transfert CPU → GPU
    persistent_workers=True,    # Garder les processus worker en vie pour éviter de recréer les processus à chaque epoch
    prefetch_factor=2,          # Nombre de batches préchargés par worker
)

8.3 Compilation de modèle torch.compile (PyTorch 2.0+)

Une ligne de code compile et optimise le graphe de calcul, améliorant la vitesse de 30%–200% (selon l'architecture du modèle) :

Exemple

# Compiler le modèle ; la première exécution a un surcoût de compilation, les itérations suivantes sont nettement plus rapides
model = torch.compile(model)

# Compromis entre les différents modes de compilation
model = torch.compile(model, mode="default")          # Équilibré, adapté à la plupart des scénarios
model = torch.compile(model, mode="reduce-overhead")  # Réduit la surcharge d'ordonnancement
model = torch.compile(model, mode="max-autotune")     # Optimisation maximale, temps de compilation plus long

8.4 Accumulation de gradient (Simulation de grands lots)

Lorsque la mémoire est insuffisante, l'accumulation de gradient peut simuler une taille de lot plus grande sans réellement augmenter l'utilisation de la mémoire :

Exemple

accumulation_steps = 8    # Met à jour les paramètres tous les 8 lots, équivalent à batch_size × 8

for i, (inputs, labels) in enumerate(train_loader):
    inputs = inputs.to(device)
    labels = labels.to(device)

    outputs = model(inputs)
    loss    = criterion(outputs, labels) / accumulation_steps   # Diviser la perte équitablement
    loss.backward()    # Accumuler les gradients sans les remettre à zéro

    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()    # Ne remettre les gradients à zéro qu'après la mise à jour des paramètres

8.5 Stratégies pour une mémoire GPU insuffisante

Stratégie Méthode Effet
Réduire la taille du lot 256 -> 64 Réduit linéairement la mémoire
Précision mixte AMP + FP16/BF16 Mémoire réduite d'environ 50%
Accumulation de gradients Mettre à jour toutes les N étapes Simuler un grand lot sans augmenter la mémoire
Checkpointing de gradient torch.utils.checkpoint Réduit considérablement la mémoire, mais réduit la vitesse d'entraînement
Geler certains paramètres Geler le backbone en apprentissage par transfert Réduit l'utilisation de la mémoire de la rétropropagation

9. Erreurs courantes et dépannage

Cette section résume les erreurs les plus courantes lors de l'entraînement sur GPU et leurs solutions.

9.1 RuntimeError : tous les tenseurs doivent être sur le même appareil

Cause : les tenseurs impliqués dans l'opération sont sur des appareils différents (l'un sur CPU, l'autre sur GPU).

Exemple

# Dépannage : afficher l'appareil de chaque tenseur
print(inputs.device, labels.device, next(model.parameters()).device)

# Solution : assurez-vous que .to(device) est exécuté pour chaque lot
inputs = inputs.to(device)
labels = labels.to(device)

9.2 RuntimeError : mémoire CUDA insuffisante

Cause : mémoire insuffisante, souvent due à une taille de lot trop grande, un modèle trop grand, ou une accumulation accidentelle des graphes de calcul.

Exemple

# Piège fréquent : loss.item() n'est pas appelé dans la boucle, provoquant l'accumulation des graphes de calcul
# Code incorrect
total_loss += loss           # loss est un tenseur qui contient tout le graphe de calcul

# Code correct
total_loss += loss.item()    # .item() extrait un scalaire Python et libère le graphe de calcul

# Autres étapes de dépannage :
# 1. Réduire batch_size
# 2. S'assurer que la boucle de validation utilise torch.no_grad()
# 3. Appeler torch.cuda.empty_cache() pour vider le cache
# 4. Utiliser torch.cuda.memory_summary() pour localiser la source de l'utilisation de la mémoire GPU

9.3 Impossible d'appeler numpy() sur un Tensor qui nécessite un gradient

Raison : les tenseurs avec gradients ne peuvent pas être convertis directement en tableaux numpy.

Exemple

# Code incorrect
arr = gpu_tensor.numpy()

# Code correct
arr = gpu_tensor.detach().cpu().numpy()

9.4 Erreur CUDA : assertion côté périphérique déclenchée

Raison : Il s'agit généralement de valeurs d'étiquette hors limites (par exemple, 10 classes mais la valeur d'étiquette est égale à 10), ou d'un indice de tableau hors limites. Le message d'erreur est généré sur le GPU, et l'emplacement d'affichage par défaut est inexact.

Exemple

# Définir la variable d'environnement pour que les erreurs soient levées à la ligne de code exacte (devient une exécution synchrone, plus lente)
import os
os.environ["CUDA_LAUNCH_BLOCKING"] = "1"

9.5 La vitesse d'entraînement ne s'améliore pas (faible utilisation du GPU)

Raison : Le chargement des données devient le goulot d'étranglement ; le GPU passe la plupart de son temps à attendre que le CPU prépare les données.

Étapes de dépannage :

  1. Exécuteznvidia-smiouwatch -n 1 nvidia-smiObservez l'utilisation du GPU
    • Une utilisation constamment inférieure à 80% indique un goulot d'étranglement des données.
  2. Augmenter le paramètre num_workers du DataLoader
  3. Activer pin_memory=True
  4. Déplacer le prétraitement des données vers le GPU (torchvision.transforms prend en charge les opérations GPU)
  5. Précharger en mémoire les jeux de données de petits fichiers

9.6 Utilisation de PyTorch Profiler pour localiser les goulots d'étranglement

Exemple

from torch.profiler import profile, ProfilerActivity

with profile(
    activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
    record_shapes=True,
) as prof:
    for i, (inputs, labels) in enumerate(train_loader):
        if i >= 10:
            break
        inputs = inputs.to(device)
        labels = labels.to(device)
        loss   = criterion(model(inputs), labels)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()

# Trier par temps CUDA, afficher les 15 premiers éléments
print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=15))

10. Référence rapide de l'API

Ce qui suit est un tableau de référence rapide pour les API courantes liées au GPU de PyTorch.

10.1 Gestion des périphériques

Opération Code
Détecter la disponibilité de CUDA torch.cuda.is_available()
Sélectionner le périphérique torch.device("cuda" if ... else "cpu")
Nombre de GPU torch.cuda.device_count()
Nom du GPU torch.cuda.get_device_name(0)
Définir le GPU actuel torch.cuda.set_device(0)
Attendre que le GPU termine torch.cuda.synchronize()

10.2 Opérations sur les tenseurs

Opération Code
Déplacer le tenseur vers le GPU tensor.to(device)outensor.cuda()
Déplacer le tenseur vers le CPU tensor.cpu()
Afficher l'appareil actuel tensor.device
Indiquer s'il est sur GPU tensor.is_cuda
Convertir un tenseur GPU en numpy tensor.detach().cpu().numpy()
Transfert asynchrone tensor.to(device, non_blocking=True)

10.3 Opérations sur le modèle

Opération Code
Déplacer le modèle vers le GPU model.to(device)
Activer le mode entraînement model.train()
Activer le mode inférence model.eval()
Accélération de la compilation torch.compile(model)
Désactiver les gradients with torch.no_grad():
Mode inférence with torch.inference_mode():

10.4 Gestion de la mémoire GPU

Opération Code
Mémoire GPU allouée torch.cuda.memory_allocated()
Mémoire GPU en cache torch.cuda.memory_reserved()
Rapport sur la mémoire GPU torch.cuda.memory_summary()
Vider le cache torch.cuda.empty_cache()

10.5 Référence rapide des principes fondamentaux

1. 用 device 变量统一管理,不要硬编码 "cuda"
2. 模型和数据必须在同一设备上,每个 batch 都要 .to(device)
3. 验证和推理时一定使用 torch.no_grad() ou  torch.inference_mode()
4. 生产训练推荐开启 AMP 混合精度,几乎免费获得 2x 加速
5. DataLoader 设置 pin_memory=True 和 num_workers >= 4 减少数据瓶颈
6. PyTorch 2.0+ 可以用 torch.compile(model) 一行提速 30%+
7. GPU 计算是异步的,精确计时需要调用 torch.cuda.synchronize()
Autres extensions