Analyse de Performance de l'Hôte
Analysez la surcharge de l'hôte/CPU dans les charges de travail d'inférence TensorRT-LLM à partir des traces nsys. Cette compétence fonctionne en deux phases :
| Phase | Question | Entrée | Sortie |
|---|---|---|---|
| Détection | La surcharge de l'hôte est-elle le goulot ? | Une seule trace nsys | Verdict OUI/NON avec preuves métriques |
| Cause Profonde | Qu'est-ce qui a régressé précisément ? | Une ou deux traces nsys | Décomposition NVTX par étape, sources de régression |
Quand l'Utiliser
- Avant de commencer un travail d'optimisation de l'hôte — confirme que le goulot est réel (Détection)
- Comme sous-étape de
perf-analysispour la classification du goulot (Détection) - Quand l'utilisation du GPU est anormalement basse et vous devez savoir pourquoi (Détection)
- Quand le débit a régressé mais les temps d'exécution des noyaux GPU sont inchangés (Cause Profonde)
- Quand l'écart entre les itérations des étapes forward a augmenté (Cause Profonde)
- Pour comparer la surcharge inter-itération entre deux versions du moteur d'inférence (Cause Profonde)
NE PAS utiliser si :
- La régression est dans la performance d'un noyau individuel (utiliser
perf-nsight-compute-analysis) - Vous devez profiler une charge de travail à partir de zéro (utiliser
workload-instrumentationd'abord) - Le problème est la communication NCCL (utiliser l'analyse distribuée)
Prérequis
- Un fichier de trace nsys (
.sqliteou.nsys-rep) d'une exécution d'évaluation TRT-LLM - Pour la comparaison Cause Profonde : deux traces (référence et cible)
- Python 3 avec support sqlite3
Concepts Clés
Surcharge de l'Hôte dans l'Inférence LLM
Dans une boucle d'inférence LLM, chaque itération se compose de :
[écart inter-étape] -> [_forward_step] -> [écart inter-étape] -> [_forward_step] -> ...
L'étape forward inclut l'exécution des noyaux GPU (GEMM, attention, normalisation, allreduce) plus la préparation côté hôte (_prepare_inputs, allocation de ressources).
L'écart inter-étape inclut tout le travail côté hôte entre les étapes forward :
- Planification des requêtes (_schedule)
- Récupération des requêtes (_fetch_new_requests)
- Diffusion des requêtes (broadcast_requests — configurations TP)
- Échantillonnage (_sample_async)
- Traitement des requêtes (_process_requests)
- Gestion des réponses (_handle_responses)
- Mises à jour de l'état des requêtes (_update_requests)
Surcharge de l'Hôte Cachée vs Exposée
La surcharge de l'hôte n'affecte les performances que lorsqu'elle est exposée — le GPU est inactif, attendant que l'hôte soumette du travail. Quand la préparation côté hôte s'exécute tandis que le GPU est encore occupé par les noyaux précédents, elle est cachée et ne coûte rien en temps réel.
Scénario A — Préparation de l'hôte cachée (lié au GPU, sain)
temps ------>
Hôte : |prep N|launch|...attente...|post|prep N+1|launch|...attente...|post|
GPU : |========= noyaux N =========|======= noyaux N+1 ======|
GPU toujours actif ; préparation hôte cachée par exécution GPU
surcharge hôte exposée = 0
Scénario B — Préparation de l'hôte exposée (lié à l'hôte, goulot)
temps ------>
Hôte : |prep N|launch|post|======= longue prep N+1 =======|launch|post|
GPU : |noyaux N| ^^^^ GPU INACTIF ^^^^ |noyaux N+1|
surcharge hôte exposée
(ajoute directement au temps réel)
Scénario C — Partiellement cachée (courant en pratique)
temps ------>
Hôte : |prep N|launch|post|======== prep N+1 ========|launch|post|
GPU : |======== noyaux N ========| ^INACTIF^ |noyaux N+1|
portion cachée -----------> exposée
(chevauche GPU, gratuit) (GPU inactif, coûte du temps réel)
Isolation de l'Étape Forward via le Motif Allreduce
Dans les configurations parallèles par tenseur, chaque étape forward exécute un nombre fixe d'opérations allreduce (une par point de communication de couche transformer).
L'algorithme :
- Trouver tous les noyaux allreduce_fusion dans la trace
- Grouper les noyaux allreduce consécutifs séparés par < 1 ms en « itérations »
- Identifier la taille d'itération la plus commune (= noyaux par étape forward)
- Détecter les limites de phase où les itérations consécutives sont séparées par > 100 ms
- Sélectionner la dernière phase avec la taille d'itération commune comme phase d'évaluation
Voir references/iteration-isolation-techniques.md pour les détails complets incluant les approches basées sur NVTX et la densité de noyaux.
Classification des Phases (Contexte vs Génération)
Les itérations TRT-LLM sont classées par le texte du marqueur NVTX en contexte (eager, pas de graphes CUDA) et génération (relecture de graphe CUDA). Les métriques agrégées peuvent masquer des goulots spécifiques à une phase, donc l'analyse par phase est cruciale.
| Phase | Condition | Caractéristiques |
|---|---|---|
| Contexte | N > 0 (toute requête ctx) |
Exécution eager, préparation hôte plus lourde |
| Génération uniquement | N == 0, M > 0 |
Relecture de graphe CUDA, préparation hôte minimale |
Format du marqueur NVTX : [Executor] _forward_step {iter}: {N} ctx reqs, {M} gen reqs
Voir references/phase-classification.md pour le code d'extraction et l'agrégation par phase.
Phase 1 : Détection (Verdict OUI/NON)
Déterminez si la surcharge de l'hôte est le goulot principal pour une charge de travail TRT-LLM.
Métriques de Détection
Six métriques, groupées en quatre catégories. Voir references/metrics.md pour les définitions complètes et les requêtes SQL.
| # | Métrique | Formule | Seuil | Ce qu'elle répond |
|---|---|---|---|---|
| M1 | Ratio d'inactivité GPU | gpu_idle_us / total_us |
> 0,30 | Le GPU est-il affamé de travail ? |
| M2 | Ratio de surcharge de lancement | cudaLaunchKernel_us / total_us |
> 0,10 | Le lancement de noyau lui-même est-il coûteux ? |
| M3a | Ratio de préparation hôte exposée | exposed_us / host_prep_total_us |
> 0,50 | La préparation hôte est-elle bien pipelinée ? |
| M3b | Impact perf préparation hôte | exposed_us / total_us |
> 0,05 | Combien la préparation exposée coûte-t-elle en débit ? |
| M3c | Attribution d'inactivité préparation hôte | exposed_us / gpu_idle_us |
> 0,50 | La préparation hôte est-elle la cause principale de l'inactivité GPU ? |
| M4 | Utilisation GPU | gpu_active_us / total_us |
< 0,60 | L'utilisation GPU est-elle trop basse ? |
| M5 | Ratio NCCL (caveat) | nccl_us / gpu_active_us |
> 0,20 | La communication est-elle un facteur de confusion ? |
Règle de confirmation de la préparation hôte : La préparation hôte est un goulot confirmé uniquement quand à la fois M3b ET M3c dépassent leurs seuils.
Les seuils sont configurables avec des variantes par phase. Voir references/thresholds.md.
Flux de Travail de Détection
Étape 1 : Validation d'Entrée
# Accepter .sqlite ou .nsys-rep
ls -la <fichier_trace>
# Si .nsys-rep, exporter vers SQLite d'abord
nsys export -t sqlite -o <sortie.sqlite> <entree.nsys-rep>
Étape 2 : Extraire les Métriques via SQL
Toutes les métriques de Détection (M1, M2, M4, M5) sont calculées directement à partir de la trace SQLite nsys en utilisant des requêtes SQL. Aucun outil externe n'est requis.
-- M1 : ratio d'inactivité GPU + M4 : utilisation GPU
-- Étape A : obtenir la fenêtre d'analyse
SELECT MIN(start) AS window_start, MAX(end) AS window_end,
(MAX(end) - MIN(start)) / 1000.0 AS total_time_us
FROM CUPTI_ACTIVITY_KIND_KERNEL;
-- Étape B : calculer le temps actif GPU (fusionner les plages de noyaux chevauchantes)
-- Exporter les paires start/end des noyaux et fusionner en Python, ou utiliser la
-- somme approximative (précise quand le chevauchement de noyaux est minimal) :
SELECT SUM(end - start) / 1000.0 AS approx_gpu_active_us
FROM CUPTI_ACTIVITY_KIND_KERNEL;
-- gpu_idle_us ≈ total_time_us - gpu_active_us
-- gpu_idle_ratio = gpu_idle_us / total_time_us (M1, seuil >0,30)
-- gpu_utilization = 1 - gpu_idle_ratio (M4, seuil <0,60)
-- M2 : ratio de surcharge de lancement
SELECT SUM(r.end - r.start) / 1000.0 AS cudaLaunchKernel_us
FROM CUPTI_ACTIVITY_KIND_RUNTIME r
JOIN StringIds s ON r.nameId = s.id
WHERE s.value = 'cudaLaunchKernel';
-- launch_overhead_ratio = cudaLaunchKernel_us / total_time_us (seuil >0,10)
-- M5 : ratio NCCL (métrique caveat)
SELECT SUM(k.end - k.start) / 1000.0 AS nccl_us
FROM CUPTI_ACTIVITY_KIND_KERNEL k
JOIN StringIds s ON k.shortName = s.id
WHERE s.value LIKE '%nccl%';
-- nccl_ratio = nccl_us / gpu_active_us (seuil >0,20)
Pour un temps actif GPU précis (fusion des plages chevauchantes), exporter les paires (start, end) des noyaux et fusionner en Python — voir references/metrics.md pour l'approche complète.
Étape 3 : Extraire les Métriques par Itération
Parser les plages NVTX _forward_step et classifier les itérations en phases contexte/génération. Voir references/phase-classification.md.
Étape 4 : Calculer M3 (Optionnel — Avancé)
M3 (Ratio de Préparation Hôte Exposée) nécessite l'intersection des plages NVTX de préparation hôte avec les écarts d'inactivité GPU — un calcul d'intersection de plages qui ne convient pas aux requêtes SQL en ligne. Cette métrique est optionnelle pour le verdict de Détection ; M1+M2+M4+M5 sont généralement suffisants.
Si M3 est nécessaire, la calculer en Python :
- Exporter les écarts d'inactivité GPU fusionnés (de la fusion de plages de l'Étape 2)
- Exporter les plages NVTX correspondant au marqueur de préparation hôte (voir references/metrics.md pour le nom de plage configurable)
- Intersectionner chaque plage NVTX avec les écarts d'inactivité pour calculer
exposed_us - Appliquer les formules M3a/M3b/M3c de references/metrics.md
Étape 5 : Calculer Toutes les Métriques et Appliquer la Logique de Décision
Verdict Agrégé :
# Métriques principales (toujours disponibles via SQL) :
core_crossed = nombre de [M1, M2, M4] qui dépassent leur seuil
# Métriques M3 optionnelles (si calculées) :
if M3 disponible :
crossed_count = core_crossed + nombre de [M3a, M3b, M3c] qui dépassent
applicable_count = 6
else :
crossed_count = core_crossed
applicable_count = 3
if crossed_count >= 2 :
aggregate_verdict = OUI
if M3 disponible ET M3b > seuil ET M3c > seuil :
host_prep_confirmed = true
if nccl_ratio > NCCL_RATIO_CAVEAT_THRESHOLD :
ajouter caveat
Verdicts par Phase : Appliquer les seuils spécifiques à la phase aux itérations contexte et génération séparément.
Verdict Global :
if aggregate_verdict == OUI ou context_verdict == OUI ou generation_verdict == OUI :
overall_verdict = OUI
else :
overall_verdict = NON
L'analyse par phase peut élever le verdict mais jamais le rétrograder.
Étape 6 : Générer le Rapport
Formater en utilisant le modèle dans references/output-format.md.
Étapes Suivantes :
- Si OUI -> Procéder à la Phase 2 (Cause Profonde) ci-dessous, puis utiliser la compétence
perf-host-optimization - Si NON -> Utiliser
perf-nsight-compute-analysispour le SOL% du noyau outrace-interpretationpour la classification complète
Phase 2 : Analyse de Cause Profonde
Identifiez quelles opérations hôte spécifiques ont régressé et de combien. Fonctionne avec une seule trace (décomposition) ou deux traces (comparaison).
Principes
-
Isoler les étapes forward, pas la trace complète. Les traces nsys contiennent le préchauffage, la compilation JIT, le chargement du modèle et l'arrêt. Seules les itérations des étapes forward représentent la performance d'inférence réelle.
-
Utiliser des motifs de noyaux structurels pour la détection d'itération. Le regroupement de noyaux allreduce est plus robuste que la densité de noyaux ou les heuristiques de fenêtre de temps.
-
Comparer les itérations en état stable. Filtrer vers les itérations avec une charge de travail identique (même taille de lot, même mélange ctx/gen) pour une comparaison propre.
-
Métriques par étape, pas les totaux. Quand les fenêtres d'évaluation diffèrent en durée ou en nombre d'étapes, toujours comparer les moyennes par étape.
Flux de Travail de Cause Profonde
Étape 1 : Collecter les Traces nsys
Profiler les deux versions (si comparaison) avec des paramètres identiques :
nsys profile -o /chemin/vers/trace \
-t cuda,nvtx,osrt \
--force-overwrite=true \
--cuda-memory-usage=true \
-w true \
<commande_benchmark> --num_requests 500
Étape 2 : Exporter vers SQLite
nsys export --type=sqlite --force-overwrite=true -o trace.sqlite trace.nsys-rep
Étape 3 : Exécuter l'Analyse de Surcharge de l'Hôte
# Comparaison de deux traces
python scripts/analyze_host_overhead.py \
--baseline /chemin/vers/trace_baseline/trace.sqlite \
--target /chemin/vers/trace_cible/trace.sqlite \
--baseline-label "v1.1" \
--target-label "main" \
--output /chemin/vers/sortie/analysis.txt
# Décomposition d'une seule trace
python scripts/analyze_host_overhead.py \
--baseline /chemin/vers/trace.sqlite \
--baseline-label "current"
Étape 4 : Interpréter les Résultats
Le script produit :
- Détection d'itération basée sur allreduce — confirme les limites des étapes forward
- Comparaison du temps réel par étape — quantifie la régression
- Décomposition NVTX par étape — identifie quelles opérations hôte ont régressé
- Comparaison des noyaux GPU — confirme que l'exécution GPU est inchangée
- Comparaison de l'API CUDA — détecte les changements de surcharge de lancement de noyau
Lire la Sortie
Temps Réel par Étape :
Temps moyen par étape : 3 317 us (baseline) vs 3 978 us (cible) +19,9 %
C'est la métrique de régression principale.
Décomposition NVTX :
Opération | baseline (us/step) | cible (us/step) | Delta | Statut
_fetch_new_requests | 36 | 270 | +234 | RÉGRESSION
broadcast_requests | - | 250 | +250 | NOUVEAU
_update_requests | 413 | 723 | +310 | RÉGRESSION
_sample_async | 1 163 | 720 | -443 | AMÉLIORATION
_process_requests | 1 056 | 390 | -666 | AMÉLIORATION
Concentrez-vous sur les opérations avec de grands deltas absolus. Vérifiez si les améliorations compensent les régressions.
Comparaison des Noyaux GPU :
Noyaux par étape (lancés) : 6,2 (baseline) vs 21,9 (cible) +253 %
Plus de lancements individuels = plus de surcharge de lancement côté hôte.
Motifs Courants et Causes Profonde
Motif 1 : Refonte de la Gestion des Requêtes
Symptôme : _fetch_new_requests a régressé 5-10x, nouvelle opération broadcast_requests.
Cause : Récupération des requêtes refactorisée pour supporter la diffusion multi-rang en TP.
Impact : +500-1 000 us/step dans les configurations TP.
Atténuation : Optimiser le chemin de diffusion ; regrouper les mises à jour d'état des requêtes.
Motif 2 : Nombre de Lancements de Noyaux Augmenté
Symptôme : 3-5x plus d'appels cudaLaunchKernel par étape, temps GPU similaire.
Cause : Les opérations qui étaient fusionnées ou capturées par graphe sont maintenant des lancements individuels.
Impact : +50-100 us/step seulement de la surcharge de lancement.
Atténuation : Re-fusionner les noyaux ; étendre la portée de capture du graphe CUDA.
Motif 3 : Nouvelles Opérations de Comptabilité
Symptôme : Nouvelles plages NVTX comme _write_finish_reasons, handle_additional_outputs.
Cause : Nouvelles fonctionnalités ajoutées à la boucle d'inférence sans budgetisation de surcharge.
Impact : +100-200 us/step chacune.
Atténuation : Reporter la comptabilité non critique vers des chemins asynchrones ; regrouper les mises à jour.
Motif 4 : Préchauffage JIT Flashinfer se Faisant Passer pour l'Inférence
Symptôme : Nombres massifs de noyaux élémentaires/réducteurs dans l'analyse « état stable ». Cause : La fenêtre d'analyse inclut la phase de compilation JIT flashinfer. Impact : Faux positif — pas une régression réelle. Détection : Les étapes forward identifiées par le motif allreduce NE contiennent PAS ces noyaux. Correction : Utiliser l'isolation d'itération basée sur allreduce, pas la densité de noyaux ou les fenêtres de temps.
Motif 5 : Goulot Contexte Uniquement (Masqué par Agrégation)
Symptôme : Métriques agrégées en dessous du seuil (ex., GPU inactif 25 %), mais les itérations de contexte ont 50 % de GPU inactif.
Cause : Les itérations de génération (saines, graphes CUDA) diluent le goulot de phase contexte.
Détection : L'analyse par phase dans la phase de Détection attrape cela.
Correction : Optimiser la préparation hôte de phase contexte (_prepare_tp_inputs chemin eager).
Pièges
1. shortName est un ID Entier
Dans CUPTI_ACTIVITY_KIND_KERNEL, shortName est un entier référençant StringIds.id. Toujours joindre :
SELECT s.value, COUNT(*), SUM(k.end - k.start)/1000.0
FROM CUPTI_ACTIVITY_KIND_KERNEL k
JOIN StringIds s ON k.shortName = s.id
WHERE k.start >= ? AND k.start < ?
GROUP BY s.value ORDER BY 3 DESC
2. NVTX textId vs text
La plupart des événements NVTX ont textId (entier) mais NULL text. Joindre avec StringIds :
SELECT s.value, n.start, n.end
FROM NVTX_EVENTS n
JOIN StringIds s ON n.textId = s.id
WHERE s.value LIKE '%_forward_step%'
3. Plages NVTX Dupliquées des Rangs TP
Dans les configurations TP, chaque rang rapporte les plages NVTX indépendamment. Dédupliquer en groupant les entrées à moins de 100 us l'une de l'autre.
4. Écarts Inter-Étape Négatifs
Quand les rangs TP rapportent des plages NVTX chevauchantes, gap = next_start - prev_end peut être négatif. Utiliser le temps de fin maximum quand on déduplique.
5. Sélection de la Fenêtre d'Évaluation
La fenêtre basée sur allreduce capture les phases contexte+génération ; le filtrage NVTX en état stable capture la génération uniquement. Les deux sont valides ; utiliser celle appropriée pour votre objectif de comparaison.
Schéma SQLite nsys Reference
Tables Clés
| Table | Objectif | Colonnes Clés |
|---|---|---|
CUPTI_ACTIVITY_KIND_KERNEL |
Exécutions de noyaux GPU | start, end, shortName (-> StringIds) |
CUPTI_ACTIVITY_KIND_RUNTIME |
Appels API CUDA | start, end, nameId (-> StringIds) |
NVTX_EVENTS |
Événements NVTX plages | start, end, textId (-> StringIds), text |
StringIds |
Table de lookup de chaîne | id, value |
Requêtes Utiles
Trouver tous les noms de plages NVTX :
SELECT DISTINCT s.value, COUNT(*)
FROM NVTX_EVENTS n
JOIN StringIds s ON n.textId = s.id
WHERE n.end > 0
GROUP BY s.value
ORDER BY COUNT(*) DESC
Chronologie des noyaux allreduce :
SELECT (k.start - (SELECT MIN(start) FROM CUPTI_ACTIVITY_KIND_KERNEL))/1e9 AS t_sec,
(k.end - k.start)/1000.0 AS dur_us
FROM CUPTI_ACTIVITY_KIND_KERNEL k
JOIN StringIds s ON k.shortName = s.id
WHERE s.value LIKE '%allreduce%'
ORDER BY k.start
Décomposition de l'API CUDA pendant une fenêtre de temps :
SELECT s.value, COUNT(*), SUM(r.end - r.start)/1000.0 AS total_us
FROM CUPTI_ACTIVITY_KIND_RUNTIME r
JOIN StringIds s ON r.nameId = s.id
WHERE r.start >= ? AND r.start < ?
GROUP BY s.value ORDER BY total_us DESC
Étude de Cas : Régression Llama 3.2 1B TP=2 (v1.1 -> main, Février 2025)
Symptôme
Régression de débit de 19,2 % : 445,63 -> 360,02 req/sec.
Faux Départs
- Analyse de trace complète a attribué la régression à des noyaux élémentaires/réducteurs massifs -> préchauffage flashinfer JIT, pas l'inférence.
- Analyse de graphe CUDA a suggéré que l'utilisation du graphe a changé -> les deux versions utilisent des motifs similaires.
- Fenêtrage par densité de noyaux a sélectionné la mauvaise fenêtre de temps -> a capturé la phase JIT.
Analyse Correcte (Détection + Cause Profonde)
La Détection a confirmé que la surcharge de l'hôte est le goulot (ratio d'inactivité GPU élevé, noyaux GPU inchangés).
La Cause Profonde via allreduce + analyse NVTX :
- Regroupement allreduce_fusion (66 par itération) a isolé les étapes forward
- Les profils de noyaux GPU étaient identiques entre les versions
- Temps réel par étape : 3 317 us -> 3 978 us (+20 %)
- Écart inter-étape P50 : 2 543 us -> 4 468 us (+76 %)
Décomposition de la Cause Profonde
| Source | Delta (us/step) | Notes |
|---|---|---|
| _update_requests | +310 | Presque doublé |
| _fetch_new_requests | +234 | 36 -> 270 us (+643 %) |
| broadcast_requests | +250 | NOUVELLE opération pour synchronisation TP |
| prepare_resources | +156 | +81 % |
| Lancements de noyaux | +57 | 3,5x plus de lancements individuels |
| _process_requests | -666 | Amélioré |
| _sample_async | -443 | Amélioré |
Leçon
La régression était entièrement dans la couche de gestion des requêtes entre les étapes forward. Le calcul GPU était inchangé. L'isolation d'itération structurelle et la comparaison NVTX en état stable étaient essentielles pour la cause profonde correcte.
Transfert vers l'Optimisation
Quand l'analyse est terminée et le verdict est OUI, transférer vers la compétence perf-host-optimization avec le contexte suivant :
-
Verdict de détection et preuves : Quelles métriques ont dépassé les seuils (M1-M5), si la préparation hôte a été confirmée comme goulot (M3b+M3c), et décomposition par phase.
-
Triage basé sur NVTX (de la sortie Cause Profonde) : Les opérations NVTX qui ont le plus régressé par delta absolu (us/step) guident quelle fonction profiler en premier avec line_profiler. Mapper les noms de plages NVTX aux fonctions sources :
_prepare_tp_inputs→PyTorchModelEngine._prepare_tp_inputs_fetch_new_requests/broadcast_requests→ gestion des requêtes dansPyExecutor_update_requests/_process_requests→ cycle de vie des requêtes dansPyExecutor_sample_async→ pipeline d'échantillonnage
-
Bloc de données de transfert : Inclure les données structurées de references/output-format.md (voir section « Transfert vers l'Optimisation ») pour que la compétence d'optimisation puisse prioriser sans ré-exécuter l'analyse.
Référence
| Fichier | Contenu |
|---|---|
| references/metrics.md | Définitions de métriques complètes, formules, requêtes SQL, analyse sous-métrique M3 |
| references/thresholds.md | Tableaux de seuils agrégés et par phase |
| references/phase-classification.md | Parsing de marqueur NVTX, classification d'itération, agrégation par phase |
| references/output-format.md | Modèle de rapport et schéma JSON d'intégration |
| references/examples.md | Six scénarios travaillés (agrégés et spécifiques à la phase) |
| references/iteration-isolation-techniques.md | Techniques d'isolation d'itération par allreduce, NVTX et densité de noyaux |
| references/trtllm-nvtx-ranges.md | Référence des plages NVTX TRT-LLM avec timings par opération |
| scripts/analyze_host_overhead.py | Script Python pour l'analyse de cause profonde automatisée |