Profilage de Workload
Référence Rapide
Choisissez UN chemin basé sur le type de workload :
| Workload | Approche | Section |
|---|---|---|
| Boucle d'entraînement | torch.cuda.synchronize() manuel + time.perf_counter() avec warmup |
Loop Workloads — Manual Timing |
| Kernel ou opération unique | Écrire un benchmark d'événement CUDA (pré-allocation, warmup, paires d'événements) | Non-Loop Workloads — CUDA Event Benchmarking |
| Ajouter des étiquettes de timeline pour nsys | Utiliser le décorateur @nvtx.annotate ou le gestionnaire de contexte |
NVTX Reference |
Principes
- Mesurez, ne devinez pas. Chaque affirmation de performance doit être traçable jusqu'à la sortie du profiler ou à des données de mesure structurées. Ne jamais inventer de métriques.
- Isolez l'état stable. Les coûts de warmup (initialisation du contexte CUDA, autotuning de cuDNN, compilation JIT) faussent les mesures. Excluez toujours les itérations de warmup avant de collecter les données.
- Utilisez la synchronisation matérielle. Les événements CUDA mesurent le temps GPU avec précision. Les minuteurs CPU (
time.perf_counter()) incluent le surcoût hôte et ratent l'exécution asynchrone. - Pas de synchronisation dans les boucles de mesure. Chaque
torch.cuda.synchronize()ajoute 10–50µs de surcoût. Enregistrez les événements CUDA de manière asynchrone, synchronisez une seule fois à la fin. - Pré-allouez tout. Tensors, événements, kernels compilés — tous avant la boucle de timing. Pour les kernels CuTe DSL, pré-compilez avec
cute.compile(). - Minimisez l'interférence du profiler. Commencez par une mesure légère (timing manuel pour latence/débit) et passez à des outils plus lourds (Kineto, nsys, ncu) uniquement quand les outils plus légers ne peuvent pas répondre à la question.
Loop Workloads — Manual Timing
Pour les boucles d'entraînement et les workloads itératifs, utilisez un timing manuel torch.cuda.synchronize() + time.perf_counter() avec warmup pour mesurer la latence par itération, le débit, et le temps de chargement de données.
Injection Template
Lisez le script d'entraînement de l'utilisateur, comprenez la structure du dataloader et de la boucle, puis injectez le code de timing.
import time
import torch
WARMUP = 5
NUM_ITERS = 30
BATCH_SIZE = 128 # global batch size for throughput calculation
iter_times = []
data_times = []
for i, batch in enumerate(dataloader):
if i >= WARMUP + NUM_ITERS:
break
t_data_end = time.perf_counter()
torch.cuda.synchronize()
t_start = time.perf_counter()
# ... existing training loop body ...
torch.cuda.synchronize()
t_end = time.perf_counter()
if i >= WARMUP:
iter_ms = (t_end - t_start) * 1000
iter_times.append(iter_ms)
if i > 0:
data_times.append((t_data_end - prev_iter_end) * 1000)
print(f"[{i:04d}]: iter {iter_ms:.2f} ms, fps {BATCH_SIZE / (iter_ms / 1000):.2f}")
prev_iter_end = t_end
import statistics
print(f"Average: iter {statistics.mean(iter_times):.2f} ms, "
f"fps {BATCH_SIZE / (statistics.mean(iter_times) / 1000):.2f}")
Interprétation des Résultats
- iter (ms) : Temps écoulé par itération (calcul + communication, excluant le chargement de données)
- data (ms) : Temps passé dans le dataloader entre les itérations. Si
data / iter > 0,2, le chargement de données est un goulot d'étranglement. - fps : Débit global en samples/seconde. À utiliser avec FLOPs-par-sample connus pour calculer le MFU.
Limitations
Le timing manuel rapporte le timing d'itération agrégé — pas la décomposition par sous-phase (forward, backward, optimizer). Quand l'utilisateur demande où le temps est dépensé dans le calcul :
- Ajoutez
torch.cuda.synchronize()+time.perf_counter()autour de chaque sous-phase pour un diagnostic ponctuel, OU - Ajoutez des annotations NVTX et lancez
nsys profilepour la visualisation de timeline.
Non-Loop Workloads — CUDA Event Benchmarking
Pour des kernels uniques, une inférence one-shot, ou des opérations autonomes, écrivez le code de benchmark d'événement CUDA directement.
PyTorch : Simple (Moyenne Uniquement)
import torch
def benchmark(fn, warmup=50, iters=100):
for _ in range(warmup):
fn()
torch.cuda.synchronize()
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
start.record()
for _ in range(iters):
fn()
end.record()
torch.cuda.synchronize()
return start.elapsed_time(end) / iters # ms per iteration
PyTorch : Détaillé (Statistiques par Itération)
import torch
import statistics
def benchmark_detailed(fn, warmup=50, iters=100):
for _ in range(warmup):
fn()
torch.cuda.synchronize()
starts = [torch.cuda.Event(enable_timing=True) for _ in range(iters)]
ends = [torch.cuda.Event(enable_timing=True) for _ in range(iters)]
for i in range(iters):
starts[i].record()
fn()
ends[i].record()
torch.cuda.synchronize()
times = [starts[i].elapsed_time(ends[i]) for i in range(iters)]
return {
"mean_ms": statistics.mean(times),
"median_ms": statistics.median(times),
"std_ms": statistics.stdev(times) if len(times) > 1 else 0,
"min_ms": min(times),
"max_ms": max(times),
}
Anti-Patterns
| Anti-Pattern | Problème |
|---|---|
torch.cuda.synchronize() avant ET après chaque itération |
Ajoute ~10–50µs de surcoût par itération |
time.perf_counter() pour le timing GPU |
Mesure le temps CPU, rate l'exécution GPU asynchrone |
| Absence de warmup | Les premières itérations incluent JIT, rampe d'horloge, initialisation de contexte |
| Allocation de tensors dans la boucle de mesure | Le surcoût d'allocation pollue le timing |
| Rapport de la moyenne uniquement | Masque la variance, les valeurs aberrantes, les distributions bimodales |
Pour des templates de benchmarking supplémentaires (CUDA Graph, CuTe DSL, Triton, Raw CUDA), voir references/benchmarking-patterns.md.
NVTX Reference
NVTX (NVIDIA Tools Extension) ajoute des annotations nommées aux timelines du profiler. Utilisez NVTX pour étiqueter les phases (forward, backward, optimizer) pour la lisibilité dans nsys — non pour la mesure.
import nvtx
# Décorateur — annote chaque appel
@nvtx.annotate("training_step", color="blue")
def training_step():
...
# Gestionnaire de contexte — annote un bloc de code
with nvtx.annotate("data_loading", color="green"):
batch = next(dataloader)
- Faites annoter les phases d'entraînement (forward, backward, optimizer, chargement de données) pour la clarté de la timeline nsys.
- Ne faites pas annoter pour la mesure — utilisez les événements CUDA ou le timing manuel à la place.
- Ne faites pas sur-annoter — trop de ranges fine-grained ajoute du désordre visuel et du surcoût mineur.
Pour les domaines, catégories, payloads et détails API hérités de NVTX, voir references/nvtx-api.md.
Références
- references/benchmarking-patterns.md — Templates CUDA Graph, CuTe DSL, Triton, Raw CUDA ; orientation warmup ; propriétés du matériel GPU ; format de rapport
- references/nvtx-api.md — Domaines, catégories, payloads, API push/pop hérités
- references/pytorch-profiler-api.md — Changements de l'API du profiler PyTorch 2.0+ (
device_timevscuda_timedéprécié)