perf-torch-sync-free

Par nvidia · skills

Identifiez et éliminez les synchronisations host-device dans le code PyTorch. Détecte les points de synchronisation (`.item()`, `.cpu()`, l'indexation booléenne, `torch.tensor` sur CUDA), classe les fausses dépendances des vraies, propose des alternatives sans synchronisation. Déclencheurs : sync-free, synchronization, `.item()`, `.cpu()`, host-device sync, eliminate syncs, CPU stall, `non_blocking`, `set_sync_debug_mode`, `cudaStreamSynchronize`, `cudaEventSynchronize`, remove syncs, async GPU.

npx skills add https://github.com/nvidia/skills --skill perf-torch-sync-free

Écrire du Code PyTorch Sans Synchronisation

Le code sans synchronisation signifie que le CPU met continuellement du travail en file d'attente sur le GPU sans attendre que les opérations GPU se terminent. Quand les synchronisations hôte-appareil sont éliminées, le GPU fonctionne continuellement sans blocages d'inactivité.

Chaque synchronisation hôte-appareil appelle finalement l'une des trois API du driver CUDA qui bloquent le thread CPU :

  • cuEventSynchronize -- le CPU attend jusqu'à ce qu'un événement GPU spécifique se termine
  • cuStreamSynchronize -- le CPU attend jusqu'à ce que tout le travail sur un stream se termine
  • cuCtxSynchronize -- le CPU attend jusqu'à ce que tout le travail sur tous les streams se termine

Quand l'utiliser

Utilisez cette compétence quand vous rencontrez :

  • Déclencheurs : L'utilisateur veut supprimer les synchronisations hôte-appareil, éliminer les blocages CPU dus aux attentes GPU, rendre le code async/sans-sync, supprimer les appels .item() ou .cpu() qui bloquent le CPU, ou comprendre pourquoi certaines opérations PyTorch causent une synchronisation
  • Symptômes : cudaStreamSynchronize fréquent dans les profils nsys, avertissements de torch.cuda.set_sync_debug_mode, débit d'entraînement limité par les allers-retours CPU-GPU, appels .item() ou .cpu() dans les boucles critiques
  • Mots-clés : "sync-free", "synchronization", ".item()", ".cpu()", "host-device sync", "eliminate syncs", "CPU stall", "non_blocking", "set_sync_debug_mode", "cudaStreamSynchronize", "cudaEventSynchronize", "remove syncs", "async GPU", "CPU waiting on GPU"

N'utilisez PAS cette compétence pour :

  • Appliquer CUDA Graphs ou réduire la surcharge de lancement de kernels (utilisez perf-torch-cuda-graphs à la place)
  • Profiler des kernels GPU, des chronologies système ou trouver le temps d'inactivité du GPU (utilisez perf-nsight-compute-analysis ou perf-nsight-systems)
  • Optimisation de kernels ou génération de code (utilisez kernel-triton-writing)
  • Optimiser la communication NCCL ou les opérations collectives d'entraînement distribué
  • Réduire l'utilisation mémoire GPU ou le gradient checkpointing
  • Compilation générale de modèle avec torch.compile

Dépendances

Dépendance Version Notes
PyTorch >=2.0 Avec support CUDA
GPU NVIDIA Quelconque Compatible CUDA
Nsight Systems Optionnel Pour la détection exhaustive des syncs via nsys

Flux de travail

Étape 1 : Détecter les Synchronisations

Utilisez une ou les deux méthodes pour trouver les points de synchronisation dans le code.

Détection rapide -- Le mode débogage de synchronisation PyTorch affiche un avertissement avec une trace de pile à chaque synchronisation :

import torch

# Activez au début de la région que vous voulez vérifier
torch.cuda.set_sync_debug_mode('warn')   # affiche l'avertissement + trace de pile
# torch.cuda.set_sync_debug_mode('error')  # lève une exception lors d'une sync

# Exécutez votre étape d'entraînement / forward pass ici
train_step(model, batch)

torch.cuda.set_sync_debug_mode(0)  # désactiver

Ce mode détecte uniquement les syncs passant par le cuStreamSynchronize enrobé de PyTorch. Les bibliothèques tierces appelant directement les API CUDA sync ne sont pas détectées.

Détection exhaustive -- Nsight Systems capture tous les appels sync y compris ceux des extensions et bibliothèques :

nsys profile --capture-range=cudaProfilerApi \
             --python-sampling=true \
             --backtrace=dwarf \
             python your_script.py

Dans l'interface graphique Nsight Systems, vérifiez la ligne de chronologie CUDA API et recherchez cudaStreamSynchronize, cudaEventSynchronize ou cudaDeviceSynchronize. Le panneau de pile d'appels montre quelle ligne Python a déclenché chaque sync.

Étape 2 : Classifier -- Fausses vs Vraies Dépendances

Après détection des syncs, classifiez chacune avant de décider comment la corriger.

Fausses dépendances (évitables) -- Le CPU n'a pas réellement besoin du résultat GPU. Elles peuvent être éliminées sans changer la logique du programme :

  • Affichages de débogage laissés dans les chemins critiques (print(loss.item()))
  • Appels .item() inutiles pour la journalisation qui pourraient être différés
  • Utiliser .cuda() au lieu de .to('cuda', non_blocking=True)
  • Utiliser .type(torch.LongTensor) au lieu de .type(torch.long)
  • Créer des tenseurs directement sur CUDA à partir d'objets Python

Vraies dépendances (nécessitent une restructuration) -- Le CPU a réellement besoin de la valeur GPU pour continuer :

  • Dépendance de flux de contrôle : if loss.item() > threshold: -- le CPU se branche sur une valeur calculée par le GPU
  • Allocation mémoire dynamique : output = x[mask] -- la taille de la sortie dépend du calcul GPU
  • Calcul CPU utilisant des valeurs GPU : calcul de statistiques pour la journalisation, mise à jour des taux d'apprentissage à partir des métriques

Les vraies dépendances nécessitent une restructuration : déplacer la logique vers le GPU (torch.where()), différer jusqu'à la fin de l'itération, ou accepter que ces parties restent en dehors de toute région de capture CUDA Graph.

Étape 3 : Éliminer Systématiquement

Appliquez les corrections par ordre de difficulté croissante. Commencez par les gains faciles.

1. Supprimer la redondance -- Supprimez les opérations qui n'ont pas besoin de se produire :

  • Supprimez les affichages de débogage et la journalisation des boucles critiques
  • Supprimez les appels .item() inutiles
  • Éliminez les synchronisations en double

2. Utiliser non_blocking=True -- Rendez les transferts asynchrones quand le CPU n'utilise pas immédiatement le résultat :

# Avant (syncs)
x_gpu = x_cpu.cuda()
x_cpu = x_gpu.cpu()

# Après (async, pas de sync)
x_gpu = x_cpu.to('cuda', non_blocking=True)
x_cpu = x_gpu.to('cpu', non_blocking=True)   # seulement si CPU n'utilise pas x_cpu immédiatement

Utilisez non_blocking=True pour GPU-vers-CPU uniquement quand le CPU n'utilise pas immédiatement le résultat. Sinon le CPU pourrait opérer sur des données incomplètes.

3. Basculer vers les alternatives d'API sans synchronisation -- Voir le Tableau de Référence Rapide ci-dessous pour une cartographie condensée des modèles communs.

4. Différer la synchronisation à la fin de l'itération -- Déplacez la journalisation et la validation après l'étape optimiseur plutôt qu'au milieu du forward/backward :

# Avant : sync au milieu de l'itération
loss = model(batch)
print(f"Loss: {loss.item()}")    # cuStreamSynchronize
loss.backward()

# Après : différer jusqu'à la fin de l'itération
loss = model(batch)
loss.backward()
optimizer.step()
print(f"Loss: {loss.item()}")    # sync est en dehors du chemin critique

5. Fusionner plusieurs syncs en une -- Si vous avez besoin de plusieurs valeurs GPU sur le CPU, rassemblez-les et transférez une fois :

# Avant : 3 syncs séparées
loss_val = loss.item()           # cuStreamSynchronize
acc_val = accuracy.item()        # cuStreamSynchronize
gnorm_val = grad_norm.item()     # cuStreamSynchronize

# Après : 1 sync
metrics = torch.stack([loss, accuracy, grad_norm])
vals = metrics.cpu()             # seul cuStreamSynchronize
loss_val, acc_val, gnorm_val = vals.tolist()

6. Décharger la logique sur le GPU -- Remplacez la logique côté CPU par des opérations natives GPU :

# Avant : flux de contrôle CPU (syncs)
if loss.item() > threshold:
    result = a
else:
    result = b

# Après : sélection côté GPU (pas de sync)
result = torch.where(loss > threshold, a, b)

# Avant : max Python (syncs)
val = max(x_gpu[0, 0], x_gpu[0, 1])

# Après : torch.max (pas de sync)
val = torch.max(x_gpu[0, 0], x_gpu[0, 1])

7. Exclure les syncs inévitables de la région de capture (dernier recours) -- Si une sync ne peut pas être éliminée, gardez-la en dehors de la région de capture CUDA Graph et graphiquez seulement les sections sans-sync. La graphication partielle est mieux que pas de graphication.

Étape 4 : Vérifier

Réexécutez la détection pour confirmer que les syncs sont éliminées :

torch.cuda.set_sync_debug_mode('error')  # lèvera une exception si une sync reste
train_step(model, batch)
torch.cuda.set_sync_debug_mode(0)

Ou reprofilez avec Nsight Systems et confirmez qu'aucun appel cudaStreamSynchronize / cudaEventSynchronize / cudaDeviceSynchronize n'apparaît dans la région cible.

Tableau de Référence Rapide

Modèle Induisant une Sync Alternative Sans Synchronisation
Transferts d'Appareil
.cpu() ou .to('cpu') .to('cpu', non_blocking=True) (fire-and-forget seulement)
.cuda() ou .to('cuda') .to('cuda', non_blocking=True)
.type(torch.LongTensor) .type(torch.long) (conversion dtype, reste sur le GPU)
Création de Tenseur
torch.tensor(obj, device='cuda') Créer sur CPU, puis .to('cuda', non_blocking=True)
torch.tensor(0, device='cuda') torch.zeros(1, device='cuda', dtype=...).squeeze()
torch.as_tensor(arr, device='cuda') Créer sur CPU, puis .to('cuda', non_blocking=True)
torch.cuda.BoolTensor(list) torch.tensor(list, device='cpu').to('cuda', non_blocking=True)
Flux de Contrôle
.item() dans les conditionnels torch.where() ou déplacer en dehors de la région critique
if gpu_tensor: Garder la logique sur le GPU avec torch.where()
Python max(a, b) sur tenseurs GPU torch.max(a, b)
torch.is_nonzero(t) Éviter ; utiliser des comparaisons côté GPU
Indexation
x_gpu[idx_cpu] ou x_gpu[idx_list] x_gpu[idx_gpu] (garder les indices sur le même appareil)
x_gpu[idx] = 0 (assignation scalaire) x_gpu[idx] = zero_gpu (valeur tenseur GPU)
x[i:j] avec limites tenseur CUDA x[:, s] avec s = torch.arange(i, j, device='cuda')
Formes Dynamiques
x_gpu[mask_gpu] (sélection masquée) torch.where(mask_gpu, x_gpu, 0) (forme fixe)
torch.nonzero(mask) torch.where() ou déplacer en dehors de la région critique
torch.masked_select(x, mask) torch.where(mask, x, 0)
torch.unique(x) Éviter dans le chemin critique ; précalculer si possible
torch.repeat_interleave(x, r) Spécifier output_size=N si connu

Trouver Plus d'Informations

  • Tier 1 (ce fichier) : Flux de travail, classification, stratégies d'élimination, et tableau de référence rapide
  • Tier 2 (references/sync-patterns.md) : Catalogue exhaustif de modèles avec 9 catégories, exemples de code complets montrant les versions induisant et sans synchronisation, et l'API du driver CUDA spécifique déclenchée par chaque modèle

Skills similaires