Cet article s'appuie sur les résultats d'une analyse de traces GPU au niveau du noyau, réalisée sur un problème réel de PyTorch (#154318) à l'aide d'uprobes eBPF. Les bases de données de traces sont publiées dans le dépôt open source Ingero pour vérification indépendante.
TL; DR
Le DataLoader de PyTorch peut être 50 à 124 fois plus lent que l'indexation directe des tenseurs pour les charges de travail GPU en mémoire. Nous avons reproduit un problème réel de PyTorch sur une RTX 4090 et analysé chaque appel d'API CUDA et événement du noyau Linux pour en identifier la cause. Le GPU n'était pas simplement lent ; il était en réalité saturé. Les processus du DataLoader ont généré 200 000 changements de contexte CPU et 300 000 allocations de pages en 40 secondes, ce qui a contraint le GPU à attendre en moyenne 301 ms par transfert de données, alors que cela devrait prendre quelques microsecondes.
Le problème
Un utilisateur de PyTorch rapporté Le DataLoader était 7 à 22 fois plus lent que l'indexation directe des tenseurs pour une charge de travail d'inférence MLP simple. Même avec `num_workers=12`, `pin_memory=True` et `prefetch_factor=12`, l'écart restait considérable. L'utilisation du GPU oscillait entre 10 et 20 %.
Nous l'avons reproduit. L'écart était encore plus important sur notre matériel :
| Méthode | Heure | vs Direct |
|---|---|---|
| Indexation directe des tenseurs | 0.39s | 1x |
| Chargeur de données (mélange=Vrai) | 48.49s | 124x plus lent |
| Chargeur de données (optimisé, 4 processus, mémoire pincée) | 43.29s | 111x plus lent |
La charge de travail est minime : 7 millions d’échantillons, 100 caractéristiques, un MLP à 2 couches et une taille de lot de 1 million. Le modèle traite un lot en quelques millisecondes. Alors, où passe le temps ?
Ce que nvidia-smi affiche
Aucun résultat utile. L'utilisation du GPU oscille entre 0 % et 30 %. L'utilisation de la mémoire est stable. La température est normale. Le GPU est clairement sous-utilisé, mais nvidia-smi ne peut pas en indiquer la raison.
Ce que torch.profiler affiche
Le journaliste a essayé le profileur intégré de PyTorch et « n'a obtenu aucune donnée de trace significative ». C'est une frustration courante : les profileurs au niveau de l'application peuvent vous montrer quels noyaux CUDA sont en cours d'exécution, mais ils ne peuvent pas voir la planification côté hôte, la mémoire et les événements du cycle de vie des processus qui déterminent si les données arrivent à temps sur le GPU.
Ce que révèle le traçage au niveau du noyau
Nous avons exécuté le test de performance tout en traçant simultanément les appels à l'API CUDA (via des requêtes eBPF sur libcudart.so) et les événements du noyau Linux (changements de contexte du planificateur, allocations de pages mémoire, créations de processus). Les résultats sont éloquents.
Visite vidéo complète : https://asciinema.org/a/RGwhPeXAPJdhXqxp
Dans la vidéo, nous avons connecté un LLM à poids ouvert (MiniMax-M2.7) à la base de données de traces via MCP (Model Context Protocol) :
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.jsonLa configuration JSON indique au client MCP où trouver le serveur Ingero et quelle base de données de traces charger :
{ "mcpServers": { "ingero": { "command": "./bin/ingero", "args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"] } } }
Cela permet au LLM d'accéder directement aux données de traçage via sept outils : get_trace_stats, get_causal_chains, get_per_process_breakdown, et d'autres. L'IA peut interroger la base de données, corréler les événements CUDA avec les données d'ordonnancement du noyau et générer un diagnostic en langage clair sans aucune analyse manuelle.
4 chaînes causales de haute gravité
Le moteur de chaîne causale a détecté 4 schémas de haute gravité, tous ayant la même cause racine :
[ÉLEVÉ] cudaStreamSync p99=42 ms (1 638x p50=25 µs) - CPU 100 % + 1 880 événements sched_switch Chronologie : [SYSTÈME] CPU 100 % [HÔTE] 1 880 changements de contexte (21 s hors CPU) [CUDA] p99 = 42 ms (1 638 x p50 = 25 µs) Cause principale : Conflit de processus DataLoader pour le CPU, forte pression sur l’allocation de pages
[HIGH] cudaLaunchKernel p99=24.67 ms (349x p50=70 µs) - CPU 100 % Racine : 34 événements sched_switch [HIGH] cuMemAlloc p99=627 µs (4.0x p50) - CPU 100 % [HIGH] cuLaunchKernel p99=106 µs (4.0x p50) - CPU 100 %
Le p99 de cudaStreamSync est 1 638 fois supérieur au p50. Il ne s'agit pas d'une lenteur du GPU, mais du GPU qui attend des données qui n'arrivent jamais à temps.
Figure 1 : Analyse générée par l’IA après l’exécution de la commande /investigate. Le modèle a utilisé les 7 outils MCP d’Ingero pour interroger la base de données de traces et a produit une explication en langage clair avec des recommandations concrètes, directement issues des données de traces.

Décomposition par processus
C’est là que tout devient clair. Le processus principal et ses 4 processus DataLoader sont visibles comme des entités distinctes :
Processus principal:
- cudaMemcpyAsync (transfert hôte-périphérique) : moyenne 301 ms, maximum 2.9 secondes - cudaStreamSync : p99 = 42 ms (normalement 25 µs) - 1 567 changements de contexte, moyenne 16 ms hors CPU, blocage maximal de 5 secondes - 799 018 allocations de pages
Processus DataLoader 1 : 52 863 changements de contexte, 89 338 allocations de pages, blocage maximal de 5 s. Processus DataLoader 2 : 50 638 changements de contexte, 83 509 allocations de pages, blocage maximal de 5 s. Processus DataLoader 3 : 49 361 changements de contexte, 70 035 allocations de pages, blocage maximal de 5 s. Processus DataLoader 4 : 38 862 changements de contexte, 56 354 allocations de pages, blocage maximal de 5 s.
Total pour l'ensemble des travailleurs : ~191 000 changements de contexte et ~299 000 allocations de pages en 40 secondes.
Qu'est-ce que cela signifie
Les processus DataLoader effectuent trois opérations coûteuses que l'indexation directe évite totalement :
- Mélange et indexation : Avec l'option `shuffle=True` activée, DataLoader génère une permutation aléatoire des indices, puis chaque nœud de calcul sélectionne son segment. Ceci nécessite des accès mémoire aléatoires sur l'ensemble du tenseur de 7 millions d'échantillons, ce qui nuit gravement à la localité du cache et provoque des défauts de page.
- Assemblage et copie : Chaque nœud de calcul regroupe les échantillons dispersés en un tenseur de lots contigu. Cela implique l'allocation de nouvelle mémoire (allocation de pages), la copie de données à partir d'emplacements aléatoires (défauts de cache) et la sérialisation du résultat vers le processus principal via la mémoire partagée ou une file d'attente.
- En compétition pour le processeur : Sur une machine à 4 vCPU, quatre processus de travail et le processus principal entraînent une préemption constante. Chaque processus est désordonnancement 50 000 fois. Le blocage maximal est de 5 secondes, pendant lesquelles le GPU n'a rien à traiter.
Avec indexation directe : X[i:i+batch_size] est une vue sans copie d'un tenseur contigu déjà en mémoire. .to(device) déclenche un transfert DMA unique à partir d'une seule région contiguë. Aucun processus n'est utilisé, aucun brassage n'est effectué, aucun classement n'est effectué, aucune copie inter-processus n'est effectuée, aucun changement de contexte n'est effectué. Le GPU reçoit les données en microsecondes, et non en centaines de millisecondes.
La réparation
Pour les charges de travail GPU en mémoire où l'ensemble des données tient dans la RAM :
- N'utilisez pas DataLoader. L'indexation directe avec un tableau d'index pré-mélangé est plus simple et 100 fois plus rapide :
indices = torch.randperm(num_samples) pour i dans la plage(0, num_samples, batch_size): batch = X[indices[i:i+batch_size]].to(device) output = model(batch)
- Si vous devez utiliser DataLoader, Ajustez `num_workers` au nombre de cœurs de votre processeur moins un. Sur une machine à 4 cœurs, `num_workers=2` réduit les conflits. Ajoutez `persistent_workers=True` pour éviter la surcharge liée à la création de processus.
- Pour les ensembles de données plus volumineux que la mémoire disponible Lorsque DataLoader est nécessaire, le véritable goulot d'étranglement se situe au niveau des E/S disque. Utilisez prefetch_factor=2 (pas plus ; un préchargement plus important entraîne une plus grande sollicitation de la mémoire) et assurez-vous que votre stockage peut suivre.
The Bigger Picture
Cette analyse met en lumière un phénomène récurrent dans les charges de travail GPU : le GPU est rapide, mais le système hôte constitue le goulot d’étranglement, et les indicateurs du GPU ne le détectent pas. nvidia-smi signalait une faible utilisation sans pouvoir en expliquer la raison. torch.profiler a capturé les noyaux CUDA, mais n’a pas enregistré les 200 000 changements de contexte survenus dans l’espace utilisateur.
La seule façon d'obtenir une vue d'ensemble était de suivre simultanément les deux aspects : les appels à l'API CUDA au niveau de la bibliothèque et les événements de planification du noyau Linux, puis de les corréler par temps et par identifiant de processus. La chaîne causale « CPU 100 % → 1 880 sched_switch → cudaMemcpyAsync 301 ms → cudaStreamSync 42 ms » résume toute l'histoire en une seule ligne. Sans le traçage inter-piles, ce problème serait resté un mystère, comme ce fut le cas pour l'auteur du rapport initial qui a passé des semaines à le déboguer.
Figure 2 : Lorsqu’on lui demande « quel est le problème principal ? », le modèle identifie une surutilisation du processeur entraînant des retards dans la planification côté hôte. Le temps d’exécution de cudaLaunchKernel est passé de 73 µs à 25.8 ms (soit 356 fois plus lent) car le processeur n’a pas pu planifier le lancement à temps.

Essayez vous-même
Reproduire le test de référence :
import torch, time from torch.utils.data import DataLoader X = torch.randn(7_000_000, 100) model = torch.nn.Sequential( torch.nn.Linear(100, 512), torch.nn.ReLU(), torch.nn.Linear(512, 512), torch.nn.ReLU(), torch.nn.Linear(512, 10) ).cuda() # Chemin rapide start = time.time() with torch.no_grad(): for i in range(0, len(X), 1_048_576): model(X[i:i+1_048_576].cuda()) torch.cuda.synchronize() print(f'Direct: {time.time()-start:.3f}s') # Chemin lent loader = DataLoader(X, batch_size=1_048_576, shuffle=True) start = time.time() avec torch.no_grad() : pour batch dans loader : model(batch.cuda()) torch.cuda.synchronize() print(f'DataLoader : {time.time()-start :.3f}s')
Utilisez Trace avec Ingero pour voir ce qui se passe en coulisses :
git clone https://github.com/ingero-io/ingero.git cd ingero && make build sudo ./bin/ingero trace --duration 60s # dans un terminal python3 benchmark.py # dans un autre terminal ./bin/ingero explain --since 60s # après la fin du benchmark
Vous pouvez aussi ignorer la reproduction et explorer directement nos données de traçage. La base de données d'investigation (764 Ko) se trouve dans le dépôt :
# Afficher les chaînes causales de l'enquête ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m # Détail par processus (voir les processus DataLoader vs le processus principal) ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m # Connecter votre assistant IA pour une enquête interactive ./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db
Enquêter avec l'IA (recommandé). La méthode la plus rapide pour analyser les données consiste à connecter directement une IA compatible MCP à la base de données. Aucune analyse manuelle n'est nécessaire.
Créez un fichier de configuration :
cat > /tmp/ingero-mcp-dataloader.json << 'EOF' { "mcpServers": { "ingero": { "command": "./bin/ingero", "args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"] } } } EOF
Connectez ensuite votre modèle :
# Avec Ollama + MiniMax (utilisé dans la vidéo) : ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json # Avec Claude Code : claude --mcp-config /tmp/ingero-mcp-dataloader.json # Avec tout client compatible MCP : # Ajoutez la configuration ci-dessus aux paramètres MCP de votre IA
Tapez /investigate pour déclencher l'analyse guidée, ou posez une question : « Qu'est-ce qui a provoqué la saturation du GPU ? » L'IA a accès à 7 outils qui interrogent directement la base de données de traces.
GitHub: github.com/ingero-io/ingero
Numéro original : pytorch/pytorch#154318
Visite vidéo : https://asciinema.org/a/RGwhPeXAPJdhXqxp
Investigation réalisée sur TensorDock RTX 4090 (24 Go), Ubuntu 22.04, PyTorch 2.10.0+cu128.













