LeRobot documentation

Déboguer votre pipeline de processeur

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v0.6.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Déboguer votre pipeline de processeur

Les pipelines de processeurs peuvent être complexes, en particulier lors de l’enchaînement de plusieurs étapes de transformation. Contrairement aux simples appels de fonction, les pipelines manquent d’observabilité naturelle : il n’est pas facile de voir ce qui se passe entre chaque étape ni d’identifier où les choses se compliquent. Ce guide fournit des outils et des techniques de débogage conçus spécifiquement pour relever ces défis et vous aider à comprendre le flux de données à travers vos pipelines.

Nous explorerons trois approches de débogage complémentaires : les hooks pour la surveillance en temps d’exécution, le débogage pas à pas pour une inspection détaillée et la validation des fonctionnalités pour détecter les incohérences structurelles. Chacune remplit un objectif distinct et, ensemble, elles offrent une visibilité complète sur le comportement de votre pipeline.

Comprendre les hooks

Les hooks sont des fonctions appelées à des points spécifiques pendant l’exécution du pipeline. Ils offrent un moyen d’inspecter, de surveiller ou de modifier les données sans modifier le code de votre pipeline. Considérez-les comme des « écouteurs d’événements » pour votre pipeline.

Qu'est-ce qu'un Hook ?

Un hook est une fonction de rappel qui est automatiquement appelée à des moments précis pendant l’exécution du pipeline. Le concept provient de la programmation orientée événements : imaginez que vous puissiez « vous accrocher » au flux d’exécution du pipeline pour observer ou réagir à ce qui se passe.

Pensez aux hooks comme à des points de contrôle insérés dans votre pipeline. Chaque fois que le pipeline atteint l’un de ces points de contrôle, il s’interrompt brièvement pour appeler votre fonction de hook, vous offrant ainsi l’occasion d’inspecter l’état actuel, de consigner des informations et de valider les données.

Un hook est simplement une fonction qui accepte deux paramètres :

  • step_idx: int - L'index de l'étape de traitement actuelle (0, 1, 2, etc.)
  • transition: EnvTransition - La transition de données à ce point du pipeline

La beauté des hooks réside dans leur nature non invasive : vous pouvez ajouter une logique de surveillance, de validation ou de débogage sans modifier la moindre ligne de votre code de pipeline. Le pipeline reste propre et concentré sur sa logique principale, tandis que les hooks gèrent les préoccupations transversales telles que la journalisation, la surveillance et le débogage.

Hooks avant et après

Le pipeline prend en charge deux types de hooks :

  • Avant les hooks (register_before_step_hook) - Appelé avant l'exécution de chaque étape
  • Après les hooks (register_after_step_hook) - Appelé après la fin de chaque étape
def before_hook(step_idx: int, transition: EnvTransition):
    """Called before step processes the transition."""
    print(f"About to execute step {step_idx}")
    # Useful for: logging, validation, setup

def after_hook(step_idx: int, transition: EnvTransition):
    """Called after step has processed the transition."""
    print(f"Completed step {step_idx}")
    # Useful for: monitoring results, cleanup, debugging

processor.register_before_step_hook(before_hook)
processor.register_after_step_hook(after_hook)

Mise en œuvre d'un hook de détection de NaN

Voici un exemple pratique d’un hook qui détecte les valeurs NaN :

def check_nans(step_idx: int, transition: EnvTransition):
    """Check for NaN values in observations."""
    obs = transition.get(TransitionKey.OBSERVATION)
    if obs:
        for key, value in obs.items():
            if isinstance(value, torch.Tensor) and torch.isnan(value).any():
                print(f"NaN detected in {key} at step {step_idx}")

# Register the hook to run after each step
processor.register_after_step_hook(check_nans)

# Process your data - the hook will be called automatically
output = processor(input_data)

# Remove the hook when done debugging
processor.unregister_after_step_hook(check_nans)

Fonctionnement interne des Hooks

Comprendre le mécanisme interne vous aide à utiliser les hooks plus efficacement. Le pipeline maintient deux listes distinctes : l’une pour les hooks avant-étape et l’autre pour les hooks après-étape. Lorsque vous enregistrez un hook, il est simplement ajouté à la liste appropriée.

Lors de l'exécution, le pipeline suit une séquence stricte : pour chaque étape de traitement, il appelle d'abord tous les before-hooks dans l'ordre d'enregistrement, puis exécute la transformation réelle de l'étape, et enfin appelle tous les after-hooks dans l'ordre d'enregistrement. Cela crée une structure prévisible, semblable à un sandwich, autour de chaque étape.

L’idée clé est que les hooks ne modifient pas la logique du pipeline principal : ils sont purement additifs. La méthode _forward du pipeline orchestre cette interaction entre les hooks et les étapes de traitement, garantissant que votre code de débogage ou de surveillance s’exécute aux moments exacts sans interférer avec le flux de données principal.

Voici une vue simplifiée de la manière dont le pipeline exécute les hooks :

class DataProcessorPipeline:
    def __init__(self):
        self.steps = [...]
        self.before_step_hooks = []  # List of before hooks
        self.after_step_hooks = []   # List of after hooks

    def _forward(self, transition):
        """Internal method that processes the transition through all steps."""
        for step_idx, processor_step in enumerate(self.steps):
            # 1. Call all BEFORE hooks
            for hook in self.before_step_hooks:
                hook(step_idx, transition)

            # 2. Execute the actual processing step
            transition = processor_step(transition)

            # 3. Call all AFTER hooks
            for hook in self.after_step_hooks:
                hook(step_idx, transition)

        return transition

    def register_before_step_hook(self, hook_fn):
        self.before_step_hooks.append(hook_fn)

    def register_after_step_hook(self, hook_fn):
        self.after_step_hooks.append(hook_fn)

Flux d'exécution

Le flux d'exécution est le suivant :

Input → Before Hook → Step 0 → After Hook → Before Hook → Step 1 → After Hook → ... → Output

Par exemple, avec 3 étapes et les deux types de hooks :

def timing_before(step_idx, transition):
    print(f"⏱️  Starting step {step_idx}")

def validation_after(step_idx, transition):
    print(f"✅ Completed step {step_idx}")

processor.register_before_step_hook(timing_before)
processor.register_after_step_hook(validation_after)

# This will output:
# ⏱️  Starting step 0
# ✅ Completed step 0
# ⏱️  Starting step 1
# ✅ Completed step 1
# ⏱️  Starting step 2
# ✅ Completed step 2

Plusieurs Hooks

Vous pouvez enregistrer plusieurs hooks du même type - ils s'exécutent dans l'ordre d'enregistrement :

def log_shapes(step_idx: int, transition: EnvTransition):
    obs = transition.get(TransitionKey.OBSERVATION)
    if obs:
        print(f"Step {step_idx} observation shapes:")
        for key, value in obs.items():
            if isinstance(value, torch.Tensor):
                print(f"  {key}: {value.shape}")

processor.register_after_step_hook(check_nans)      # Executes first
processor.register_after_step_hook(log_shapes)     # Executes second

# Both hooks will be called after each step in registration order
output = processor(input_data)

Bien que les hooks soient excellents pour surveiller des problèmes spécifiques (comme la détection de NaN) ou pour collecter des métriques lors de l’exécution normale du pipeline, il arrive qu’il faille creuser plus en profondeur. Lorsque vous souhaitez comprendre exactement ce qui se passe à chaque étape ou déboguer une logique de transformation complexe, le débogage pas à pas fournit l’inspection détaillée dont vous avez besoin.

Débogage pas à pas

Le débogage pas à pas est comparable à une lecture au ralenti de votre pipeline. Plutôt que de voir vos données se transformer en un éclair, de l'entrée à la sortie, vous pouvez mettre en pause et examiner ce qui se passe après chaque étape individuelle.

Cette approche est particulièrement précieuse lorsque vous cherchez à comprendre un pipeline complexe, à déboguer un comportement inattendu ou à vérifier que chaque transformation fonctionne comme prévu. Contrairement aux hooks, qui sont excellents pour la surveillance automatisée, le débogage pas à pas vous offre un contrôle manuel et interactif sur le processus d’inspection.

La méthode step_through() est un générateur qui produit l'état de transition après chaque étape de traitement, vous permettant d'inspecter les résultats intermédiaires. Imaginez que vous créez une série d'instantanés de vos données à mesure qu'elles traversent le pipeline : chaque instantané vous montre exactement à quoi ressemblent vos données après l'application d'une transformation supplémentaire.

Fonctionnement de l'exécution pas à pas

La méthode step_through() modifie fondamentalement l'exécution du pipeline. Au lieu d'exécuter toutes les étapes de manière séquentielle et de ne renvoyer que le résultat final, elle transforme le pipeline en un itérateur qui produit des résultats intermédiaires.

Voici ce qui se passe en interne : la méthode commence par convertir vos données d’entrée dans le format de transition interne du pipeline, puis renvoie cet état initial. Ensuite, elle applique la première étape de traitement et renvoie le résultat. Puis elle applique la deuxième étape à ce résultat et renvoie à nouveau, et ainsi de suite. Chaque yield vous fournit un instantané complet de la transition à ce point précis.

Ce motif de générateur est puissant car il est paresseux : le pipeline ne calcule l’étape suivante que lorsque vous la demandez. Cela signifie que vous pouvez vous arrêter à tout moment, inspecter l’état actuel en profondeur et décider s’il faut continuer. Vous n’êtes pas obligé d’exécuter l’ensemble du pipeline pour déboguer une seule étape problématique.

Plutôt que d’exécuter le pipeline complet et de ne voir que le résultat final, step_through() met en pause après chaque étape et vous fournit la transition intermédiaire :

# This creates a generator that yields intermediate states.
# The first yield is the input itself; yield i (i >= 1) is the output of step i - 1.
for i, intermediate_result in enumerate(processor.step_through(input_data)):
    print("=== Input ===" if i == 0 else f"=== After step {i - 1} ===")

    # Inspect the observation at this stage
    obs = intermediate_result.get(TransitionKey.OBSERVATION)
    if obs:
        for key, value in obs.items():
            if isinstance(value, torch.Tensor):
                print(f"{key}: shape={value.shape}, dtype={value.dtype}")

Débogage interactif avec des points d’arrêt

Vous pouvez ajouter des points d’arrêt dans la boucle d’exécution pas à pas pour déboguer de manière interactive :

# Step through the pipeline with debugging
for i, intermediate in enumerate(processor.step_through(data)):
    print("Input" if i == 0 else f"After step {i - 1}: {processor.steps[i - 1].__class__.__name__}")

    # Set a breakpoint to inspect the current state
    breakpoint()  # Debugger will pause here

    # You can now inspect 'intermediate' in the debugger:
    # - Check tensor shapes and values
    # - Verify expected transformations
    # - Look for unexpected changes

Pendant la session de débogage, vous pouvez :

  • Examinez intermediate[TransitionKey.OBSERVATION] pour voir les données d'observation
  • Vérifiez intermediate[TransitionKey.ACTION] pour les transformations d'action
  • Inspectez n'importe quelle partie de la transition pour comprendre ce que fait chaque étape

Le débogage pas à pas est idéal pour comprendre les transformations des données, mais qu’en est-il de la structure de ces données ? Si les hooks et le débogage pas à pas vous aident à diagnostiquer le comportement à l’exécution, vous devez également vous assurer que votre pipeline produit des données au format attendu par les composants en aval. C’est ici que la validation du contrat de caractéristiques intervient.

Validation des contrats de fonctionnalités

Les contrats de fonctionnalité définissent la structure de données que votre pipeline attend en entrée et produit en sortie. La validation de ces contrats aide à détecter les incohérences à un stade précoce.

Comprendre les contrats de fonctionnalités

Chaque étape de processeur possède une méthode transform_features() qui décrit la manière dont elle modifie la structure des données :

# Get the expected output features from your pipeline
initial_features = {
    PipelineFeatureType.OBSERVATION: {
        "observation.state": PolicyFeature(type=FeatureType.STATE, shape=(7,)),
        "observation.image": PolicyFeature(type=FeatureType.VISUAL, shape=(3, 224, 224))
    },
    PipelineFeatureType.ACTION: {
        "action": PolicyFeature(type=FeatureType.ACTION, shape=(4,))
    }
}

# Check what your pipeline will output
output_features = processor.transform_features(initial_features)

print("Input features:")
for feature_type, features in initial_features.items():
    print(f"  {feature_type}:")
    for key, feature in features.items():
        print(f"    {key}: {feature.type.value}, shape={feature.shape}")

print("\nOutput features:")
for feature_type, features in output_features.items():
    print(f"  {feature_type}:")
    for key, feature in features.items():
        print(f"    {key}: {feature.type.value}, shape={feature.shape}")

Vérification des fonctionnalités attendues

Vérifiez que votre pipeline produit les caractéristiques attendues :

# Define what features you expect the pipeline to produce
expected_keys = ["observation.state", "observation.image", "action"]

print("Validating feature contract...")
for expected_key in expected_keys:
    found = False
    for feature_type, features in output_features.items():
        if expected_key in features:
            feature = features[expected_key]
            print(f"✅ {expected_key}: {feature.type.value}, shape={feature.shape}")
            found = True
            break

    if not found:
        print(f"❌ Missing expected feature: {expected_key}")

Cette validation aide à garantir que votre pipeline fonctionnera correctement avec les composants en aval qui s'attendent à des structures de données spécifiques.

Résumé

Maintenant que vous comprenez les trois approches de débogage, vous pouvez aborder tout problème de pipeline de manière systématique :

  1. Accroches - Pour la surveillance et la validation à l'exécution sans modifier le code du pipeline
  2. Exécution pas à pas - Pour inspecter les états intermédiaires et comprendre les transformations
  3. Validation des fonctionnalités - Pour s'assurer que les contrats de structure des données sont respectés

Quand utiliser chaque approche :

  • Commencez par le débogage pas à pas lorsque vous devez comprendre ce que fait votre pipeline ou lorsqu'un comportement inattendu se produit
  • Ajoutez des hooks pour une surveillance continue pendant le développement et la production afin de détecter automatiquement les problèmes
  • Utilisez la validation des fonctionnalités avant le déploiement pour vous assurer que votre pipeline fonctionne avec les composants en aval

Ces trois outils fonctionnent ensemble pour vous offrir l’observabilité complète dont les pipelines complexes manquent naturellement. Grâce aux hooks qui surveillent les problèmes, au step-through qui vous aide à comprendre le comportement et à la validation des fonctionnalités qui garantit la compatibilité, vous serez en mesure de déboguer n’importe quel pipeline avec confiance et efficacité.

Update on GitHub