LeRobot documentation
Depure seu pipeline de processador
Depure seu pipeline de processador
Os pipelines de processadores podem ser complexos, especialmente ao encadear múltiplas etapas de transformação. Diferentemente de chamadas de função simples, os pipelines carecem de observabilidade natural: não é fácil ver o que acontece entre cada etapa ou onde as coisas dão errado. Este guia fornece ferramentas e técnicas de depuração projetadas especificamente para abordar esses desafios e ajudar você a entender o fluxo de dados através dos seus pipelines.
Exploraremos três abordagens complementares de depuração: hooks para monitoramento em tempo de execução, depuração passo a passo para inspeção detalhada e validação de recursos para identificar incompatibilidades estruturais. Cada uma atende a um propósito diferente e, juntas, oferecem visibilidade completa sobre o comportamento do seu pipeline.
Entendendo os Hooks
Hooks são funções que são chamadas em pontos específicos durante a execução do pipeline. Elas fornecem uma maneira de inspecionar, monitorar ou modificar dados sem alterar o código do seu pipeline. Pense neles como “listeners de eventos” para o seu pipeline.
O que é um Hook?
Um hook é uma função de callback que é invocada automaticamente em momentos específicos durante a execução do pipeline. O conceito vem da programação orientada a eventos: imagine que você pode “se conectar” ao fluxo de execução do pipeline para observar ou reagir ao que está acontecendo.
Pense nos hooks como a inserção de pontos de verificação no seu pipeline. Cada vez que o pipeline atinge um desses pontos, ele pausa brevemente para chamar sua função de hook, dando a você a oportunidade de inspecionar o estado atual, registrar informações e validar dados.
Um hook é simplesmente uma função que aceita dois parâmetros:
step_idx: int- O índice da etapa de processamento atual (0, 1, 2, etc.)transition: EnvTransition- A transição de dados naquele ponto do pipeline
A beleza dos hooks reside em sua natureza não invasiva: você pode adicionar lógica de monitoramento, validação ou depuração sem alterar uma única linha do código do seu pipeline. O pipeline permanece limpo e focado em sua lógica principal, enquanto os hooks lidam com as preocupações transversais, como registro de logs, monitoramento e depuração.
Hooks Antes vs Depois
O pipeline suporta dois tipos de hooks:
- Hooks antes (
register_before_step_hook) - Chamado antes de cada etapa ser executada - Após os hooks (
register_after_step_hook) - Chamado após a conclusão de cada etapa
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)Implementando um gancho de detecção de NaN
Aqui está um exemplo prático de um 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)Como os Hooks Funcionam Internamente
Compreender o mecanismo interno ajuda você a usar os hooks de forma mais eficaz. O pipeline mantém duas listas separadas: uma para hooks de antes da etapa e outra para hooks de depois da etapa. Quando você registra um hook, ele é simplesmente adicionado à lista apropriada.
Durante a execução, o pipeline segue uma sequência estrita: para cada etapa de processamento, ele primeiro chama todos os before-hooks na ordem de registro, em seguida executa a transformação real da etapa e, por fim, chama todos os after-hooks na ordem de registro. Isso cria uma estrutura previsível, semelhante a um sanduíche, ao redor de cada etapa.
O ponto-chave é que os hooks não alteram a lógica central do pipeline — eles são puramente aditivos. O método _forward do pipeline orquestra essa interação entre hooks e etapas de processamento, garantindo que seu código de depuração ou monitoramento seja executado exatamente nos momentos certos, sem interferir no fluxo principal de dados.
Aqui está uma visão simplificada de como o pipeline executa os 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)Fluxo de Execução
O fluxo de execução é o seguinte:
Input → Before Hook → Step 0 → After Hook → Before Hook → Step 1 → After Hook → ... → OutputPor exemplo, com 3 etapas e ambos os 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 2Vários Hooks
Você pode registrar vários hooks do mesmo tipo - eles são executados na ordem em que foram registrados:
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)Embora os hooks sejam excelentes para monitorar problemas específicos (como a detecção de NaN) ou para coletar métricas durante a execução normal do pipeline, às vezes é necessário aprofundar-se mais. Quando você deseja entender exatamente o que acontece em cada etapa ou depurar a lógica complexa de transformação, a depuração passo a passo fornece a inspeção detalhada de que você precisa.
Depuração passo a passo
O depurador passo a passo é como ter uma repetição em câmera lenta para o seu pipeline. Em vez de assistir seus dados sendo transformados em um borrão rápido da entrada para a saída, você pode pausar e examinar o que acontece após cada etapa individual.
Essa abordagem é particularmente valiosa quando você está tentando entender um pipeline complexo, depurar um comportamento inesperado ou verificar se cada transformação está funcionando como esperado. Diferente dos hooks, que são ótimos para monitoramento automatizado, a depuração passo a passo oferece controle manual e interativo sobre o processo de inspeção.
O método step_through() é um gerador que produz o estado de transição após cada etapa de processamento, permitindo inspecionar resultados intermediários. Pense nisso como a criação de uma série de instantâneos dos seus dados à medida que fluem pelo pipeline — cada instantâneo mostra exatamente como seus dados se apresentam após a aplicação de mais uma transformação.
Como o Step-Through Funciona
O método step_through() altera fundamentalmente a forma como o pipeline é executado. Em vez de executar todas as etapas em sequência e retornar apenas o resultado final, ele transforma o pipeline em um iterador que produz resultados intermediários.
Aqui está o que acontece internamente: o método começa convertendo seus dados de entrada para o formato de transição interno do pipeline e, em seguida, retorna esse estado inicial. A seguir, aplica a primeira etapa de processamento e retorna o resultado. Depois, aplica a segunda etapa a esse resultado e retorna novamente, e assim por diante. Cada yield fornece uma captura completa da transição naquele ponto.
Esse padrão de gerador é poderoso porque é preguiçoso (lazy)—o pipeline só calcula o próximo passo quando você solicita. Isso significa que você pode parar a qualquer ponto, inspecionar o estado atual a fundo e decidir se deve continuar. Você não é forçado a executar o pipeline inteiro apenas para depurar um passo problemático.
Em vez de executar o pipeline inteiro e ver apenas o resultado final, step_through() pausa após cada etapa e fornece a transição intermediária:
# 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}")Depuração interativa com pontos de interrupção
Você pode adicionar pontos de interrupção no loop de execução passo a passo para depurar interativamente:
# 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 a sessão do depurador, você pode:
- Examine
intermediate[TransitionKey.OBSERVATION]para ver os dados de observação - Verifique
intermediate[TransitionKey.ACTION]para transformações de ação - Inspecione qualquer parte da transição para entender o que cada etapa faz
O depurador passo a passo é perfeito para entender as transformações de dados, mas e quanto à estrutura desses dados? Embora os hooks e o depurador passo a passo ajudem a depurar o comportamento em tempo de execução, você também precisa garantir que seu pipeline produza dados no formato esperado pelos componentes a jusante. É aqui que a validação do contrato de recursos entra em cena.
Validando Contratos de Recursos
Os contratos de recursos definem a estrutura de dados que seu pipeline espera como entrada e produz como saída. A validação desses contratos ajuda a identificar incompatibilidades precocemente.
Entendendo os Contratos de Recursos
Cada etapa do processador possui um método transform_features() que descreve como ele altera a estrutura dos dados:
# 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}")Verificando os recursos esperados
Verifique se o seu pipeline produz os recursos que você espera:
# 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}")Essa validação ajuda a garantir que seu pipeline funcionará corretamente com os componentes downstream que esperam estruturas de dados específicas.
Resumo
Agora que você entende as três abordagens de depuração, pode resolver qualquer problema de pipeline de forma sistemática:
- Hooks - Para monitoramento e validação em tempo de execução sem modificar o código do pipeline
- Execução passo a passo - Para inspecionar estados intermediários e entender as transformações
- Validação de recursos - Para garantir que os contratos de estrutura de dados sejam atendidos
Quando usar cada abordagem:
- Comece com depuração passo a passo quando precisar entender o que seu pipeline faz ou quando algo inesperado acontecer
- Adicione hooks para monitoramento contínuo durante o desenvolvimento e a produção para capturar problemas automaticamente
- Use a validação de recursos antes da implantação para garantir que seu pipeline funcione com os componentes downstream
Essas três ferramentas funcionam em conjunto para fornecer a observabilidade completa que os pipelines complexos naturalmente carecem. Com hooks monitorando problemas, step-through ajudando a entender o comportamento e a validação de recursos garantindo a compatibilidade, você será capaz de depurar qualquer pipeline com confiança e eficiência.
Update on GitHub