perf-workload-profiling

Par nvidia · skills

Instrumentation de code pour le chronométrage des charges de travail. Deux scénarios : (1) Boucle d'entraînement — injection de chronométrage manuel pour rapporter la latence par itération, le débit (samples/sec) et le temps de chargement des données. (2) Kernel/op autonome — écriture de code de chronométrage par événements CUDA avec échauffement, statistiques par itération et évitement des anti-patterns. Couvre également les annotations NVTX pour l'étiquetage des timelines du profiler. PAS pour : exécuter ou analyser des outils de profiling (nsys, ncu, Nsight Systems, Nsight Compute), écrire des kernels (Triton, CuTe, CUDA), appliquer des optimisations (CUDA Graphs, gradient checkpointing, fusion), ou interpréter les métriques roofline/SOL%. Déclencheurs : « measure throughput », « benchmark this function », « time my training loop », « samples per second », « NVTX annotate », « instrument my dataloader », « data load time », « kernel timing », « how do I time ».

npx skills add https://github.com/nvidia/skills --skill perf-workload-profiling

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 :

  1. Ajoutez torch.cuda.synchronize() + time.perf_counter() autour de chaque sous-phase pour un diagnostic ponctuel, OU
  2. Ajoutez des annotations NVTX et lancez nsys profile pour 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

Skills similaires