LeRobot documentation
Depura tu pipeline de procesador
Depura tu pipeline de procesador
Los pipelines de procesadores pueden ser complejos, especialmente al encadenar múltiples pasos de transformación. A diferencia de las llamadas a funciones simples, los pipelines carecen de observabilidad natural: no es fácil ver qué ocurre entre cada paso ni dónde se producen los errores. Esta guía ofrece herramientas y técnicas de depuración diseñadas específicamente para abordar estos desafíos y ayudarte a comprender el flujo de datos a través de tus pipelines.
Exploraremos tres enfoques complementarios de depuración: hooks para el monitoreo en tiempo de ejecución, depuración paso a paso para una inspección detallada y validación de características para detectar discrepancias estructurales. Cada uno cumple una función diferente y, en conjunto, proporcionan una visibilidad completa del comportamiento de su pipeline.
Comprender los hooks
Los hooks son funciones que se invocan en puntos específicos durante la ejecución de la pipeline. Proporcionan una forma de inspeccionar, monitorear o modificar datos sin cambiar el código de tu pipeline. Piensa en ellos como “listeners de eventos” para tu pipeline.
¿Qué es un Hook?
Un hook es una función de callback que se invoca automáticamente en momentos específicos durante la ejecución de la pipeline. El concepto proviene de la programación orientada a eventos: imagine que puede “engancharse” al flujo de ejecución de la pipeline para observar o reaccionar a lo que está ocurriendo.
Piensa en los hooks como puntos de control insertados en tu pipeline. Cada vez que el pipeline llega a uno de estos puntos, se detiene brevemente para llamar a tu función de hook, dándote la oportunidad de inspeccionar el estado actual, registrar información y validar los datos.
Un hook es simplemente una función que acepta dos parámetros:
step_idx: int- El índice del paso de procesamiento actual (0, 1, 2, etc.)transition: EnvTransition- La transición de datos en ese punto de la canalización
La belleza de los hooks radica en su naturaleza no invasiva: puedes añadir lógica de monitoreo, validación o depuración sin cambiar una sola línea del código de tu pipeline. El pipeline permanece limpio y enfocado en su lógica principal, mientras que los hooks se encargan de las preocupaciones transversales como el registro, el monitoreo y la depuración.
Ganchos antes y después
El pipeline admite dos tipos de hooks:
- Antes de los hooks (
register_before_step_hook) - Se llama antes de que se ejecute cada paso - Después de los hooks (
register_after_step_hook) - Se llama después de que cada paso se completa
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)Implementar un gancho de detección de NaN
Aquí hay un ejemplo práctico de un hook que detecta valores 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)Cómo funcionan los Hooks internamente
Comprender el mecanismo interno le permite usar los hooks de manera más efectiva. El pipeline mantiene dos listas separadas: una para los hooks de before-step y otra para los hooks de after-step. Cuando registra un hook, simplemente se agrega a la lista correspondiente.
Durante la ejecución, el pipeline sigue una secuencia estricta: para cada paso de procesamiento, primero llama a todos los before-hooks en el orden de registro, luego ejecuta la transformación real del paso y, finalmente, llama a todos los after-hooks en el orden de registro. Esto crea una estructura predecible, similar a un sándwich, alrededor de cada paso.
La clave es que los hooks no modifican la lógica central del pipeline: son puramente aditivos. El método _forward del pipeline orquesta esta interacción entre hooks y pasos de procesamiento, garantizando que tu código de depuración o monitoreo se ejecute en los momentos exactos sin interferir con el flujo principal de datos.
Aquí tienes una vista simplificada de cómo el pipeline ejecuta los 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)Flujo de ejecución
El flujo de ejecución es el siguiente:
Input → Before Hook → Step 0 → After Hook → Before Hook → Step 1 → After Hook → ... → OutputPor ejemplo, con 3 pasos y ambos tipos de hook:
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 2Varios Hooks
Puedes registrar varios hooks del mismo tipo: se ejecutan en el orden en que se registraron:
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)Si bien los hooks son excelentes para monitorear problemas específicos (como la detección de NaN) o para recopilar métricas durante la ejecución normal de la tubería, a veces es necesario profundizar más. Cuando se desea comprender exactamente qué ocurre en cada paso o depurar una lógica de transformación compleja, la depuración paso a paso proporciona la inspección detallada que se necesita.
Depuración paso a paso
El depuración paso a paso es como tener una repetición en cámara lenta de tu pipeline. En lugar de ver cómo tus datos se transforman en un rápido destello de la entrada a la salida, puedes pausar y examinar lo que ocurre después de cada paso individual.
Este enfoque resulta especialmente valioso cuando se intenta comprender un pipeline complejo, depurar un comportamiento inesperado o verificar que cada transformación funcione como se espera. A diferencia de los hooks, que son excelentes para el monitoreo automatizado, la depuración paso a paso ofrece un control manual e interactivo sobre el proceso de inspección.
El método step_through() es un generador que produce el estado de transición después de cada paso de procesamiento, lo que permite inspeccionar los resultados intermedios. Piense en ello como la creación de una serie de instantáneas de sus datos a medida que fluyen a través de la tubería: cada instantánea le muestra exactamente cómo se ven sus datos después de aplicar una transformación más.
Cómo funciona el paso a paso
El método step_through() cambia fundamentalmente la forma en que se ejecuta el pipeline. En lugar de ejecutar todos los pasos en secuencia y devolver solo el resultado final, transforma el pipeline en un iterador que produce resultados intermedios.
Esto es lo que ocurre internamente: el método comienza convirtiendo los datos de entrada al formato de transición interno del pipeline y luego genera este estado inicial. A continuación, aplica el primer paso de procesamiento y genera el resultado. Después, aplica el segundo paso a ese resultado y genera de nuevo, y así sucesivamente. Cada yield te proporciona una instantánea completa de la transición en ese punto.
Este patrón de generador es potente porque es perezoso: el pipeline solo calcula el siguiente paso cuando lo solicitas. Esto significa que puedes detenerte en cualquier punto, inspeccionar el estado actual a fondo y decidir si continuar. No estás obligado a ejecutar todo el pipeline solo para depurar un paso problemático.
En lugar de ejecutar todo el pipeline y ver solo el resultado final, step_through() se detiene después de cada paso y te muestra la transición intermedia:
# 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}")Depuración interactiva con puntos de interrupción
Puede agregar puntos de interrupción en el bucle de depuración paso a paso para depurar de forma interactiva:
# 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 changesDurante la sesión del depurador, puedes:
- Examine
intermediate[TransitionKey.OBSERVATION]para ver los datos de observación - Comprueba
intermediate[TransitionKey.ACTION]para las transformaciones de acción - Inspecciona cualquier parte de la transición para comprender lo que hace cada paso
La depuración paso a paso es perfecta para comprender las transformaciones de datos, pero ¿qué pasa con la estructura de esos datos? Si bien los hooks y la depuración paso a paso te ayudan a depurar el comportamiento en tiempo de ejecución, también debes asegurarte de que tu pipeline produzca datos en el formato esperado por los componentes posteriores. Aquí es donde entra en juego la validación del contrato de características.
Validación de contratos de características
Los contratos de características definen la estructura de datos que su pipeline espera como entrada y produce como salida. Validar estos contratos ayuda a detectar discrepancias de forma temprana.
Comprensión de los contratos de características
Cada paso del procesador tiene un método transform_features() que describe cómo modifica la estructura de los datos:
# 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}")Verificación de las características esperadas
Comprueba que tu pipeline produce las características que esperas:
# 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}")Esta validación ayuda a garantizar que tu pipeline funcione correctamente con los componentes posteriores que esperan estructuras de datos específicas.
Resumen
Ahora que comprende los tres enfoques de depuración, puede abordar cualquier problema de la pipeline de forma sistemática:
- Ganchos - Para el monitoreo y la validación en tiempo de ejecución sin modificar el código de la tubería
- Inspección paso a paso - Para examinar estados intermedios y comprender las transformaciones
- Validación de características - Para garantizar que se cumplan los contratos de estructura de datos
Cuándo usar cada enfoque:
- Comience con depuración paso a paso cuando necesite comprender lo que hace su pipeline o cuando ocurra algo inesperado
- Agregue hooks para el monitoreo continuo durante el desarrollo y la producción para detectar problemas automáticamente
- Use feature validation antes de la implementación para asegurarse de que su pipeline funcione con los componentes de nivel inferior
Estas tres herramientas trabajan juntas para brindarte la observabilidad completa que los pipelines complejos carecen de forma natural. Con hooks que vigilan los problemas, step-through que te ayuda a comprender el comportamiento y la validación de características que garantiza la compatibilidad, podrás depurar cualquier pipeline con confianza y eficiencia.
Update on GitHub