LeRobot documentation
Отладка конвейера процессора
Отладка конвейера процессора
Конвейеры процессоров могут быть сложными, особенно при последовательном соединении нескольких шагов трансформации. В отличие от простых вызовов функций, конвейеры не обладают естественной наблюдаемостью: вы не можете легко увидеть, что происходит между каждым шагом или где именно возникают ошибки. В этом руководстве представлены инструменты и методы отладки, специально разработанные для решения этих проблем и помогающие понять поток данных в ваших конвейерах.
Мы рассмотрим три взаимодополняющих подхода к отладке: хуки для мониторинга во время выполнения, пошаговую отладку для детального анализа и валидацию функций для выявления структурных несоответствий. Каждый из них решает свою задачу, а в совокупности они обеспечивают полную прозрачность поведения вашего конвейера.
Понимание хуков
Hooks — это функции, которые вызываются в определённых точках во время выполнения конвейера. Они позволяют проверять, отслеживать или изменять данные, не изменяя код вашего конвейера. Представьте их как «обработчики событий» для вашего конвейера.
Что такое хук?
Хук — это функция обратного вызова, которая автоматически вызывается в определённые моменты во время выполнения конвейера. Концепция заимствована из событийно-ориентированного программирования: представьте, что вы можете «подключиться» к потоку выполнения конвейера, чтобы наблюдать за происходящим или реагировать на него.
Представьте хуки как контрольные точки, вставляемые в ваш конвейер. Каждый раз, когда конвейер достигает одной из этих точек, он на мгновение приостанавливается, чтобы вызвать вашу функцию хука, предоставляя вам возможность проверить текущее состояние, записать информацию в журнал и валидировать данные.
Хук — это просто функция, принимающая два параметра:
step_idx: int— индекс текущего шага обработки (0, 1, 2 и т. д.)transition: EnvTransition— переход данных в этой точке конвейера
Красота хуков заключается в их неинвазивной природе: вы можете добавить логику мониторинга, валидации или отладки, не изменив ни одной строки кода вашего пайплайна. Пайплайн остаётся чистым и сфокусированным на своей основной логике, тогда как хуки берут на себя поперечные задачи, такие как ведение журналов, мониторинг и отладка.
Хуки Before и After
Пайплайн поддерживает два типа хуков:
- Хуки перед шагом (
register_before_step_hook) — вызываются перед выполнением каждого шага - После хуков (
register_after_step_hook) — вызывается после завершения каждого шага
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)Реализация хука обнаружения NaN
Вот практический пример хука, который обнаруживает значения 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)Как хуки работают внутри
Понимание внутреннего механизма помогает эффективнее использовать хуки. Конвейер поддерживает два отдельных списка: один для хуков перед шагом, другой — для хуков после шага. При регистрации хук просто добавляется в соответствующий список.
В процессе выполнения конвейер следует строгому порядку: для каждого шага обработки он сначала вызывает все before-хуки в порядке регистрации, затем выполняет фактическое преобразование шага и, наконец, вызывает все after-хуки в порядке регистрации. Это создает предсказуемую, «сэндвичевую» структуру вокруг каждого шага.
Ключевая идея состоит в том, что хуки не изменяют логику основного конвейера — они носят исключительно добавочный характер. Метод _forward конвейера управляет взаимодействием между хуками и этапами обработки, гарантируя, что ваш код для отладки или мониторинга выполняется в точно нужные моменты, не мешая основному потоку данных.
Ниже представлено упрощённое описание того, как конвейер выполняет хуки:
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)Поток выполнения
Поток выполнения выглядит следующим образом:
Input → Before Hook → Step 0 → After Hook → Before Hook → Step 1 → After Hook → ... → OutputНапример, при 3 шагах и обоих типах хуков:
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Несколько хуков
Вы можете зарегистрировать несколько хуков одного типа — они выполняются в порядке регистрации:
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)Хуки отлично подходят для мониторинга конкретных проблем (например, обнаружения NaN) или сбора метрик во время обычного выполнения конвейера, но иногда требуется более глубокое погружение. Когда нужно понять, что именно происходит на каждом шаге, или отладить сложную логику трансформаций, пошаговая отладка предоставляет необходимую детальную инспекцию.
Пошаговая отладка
Пошаговая отладка — это как замедленная перемотка для вашего конвейера. Вместо того чтобы наблюдать, как данные превращаются в один быстрый миг от входа к выходу, вы можете приостановить процесс и изучить, что происходит после каждого отдельного шага.
Этот подход особенно полезен, когда вы пытаетесь разобраться в сложном конвейере, отладить неожиданное поведение или убедиться, что каждое преобразование работает как ожидается. В отличие от хуков, которые отлично подходят для автоматического мониторинга, пошаговая отладка предоставляет вам ручной интерактивный контроль над процессом проверки.
Метод step_through() является генератором, который возвращает состояние перехода после каждого шага обработки, позволяя вам проверять промежуточные результаты. Представьте, что вы создаёте серию снимков ваших данных по мере их прохождения через конвейер — каждый снимок показывает, как выглядят ваши данные после применения ещё одного преобразования.
Как работает пошаговое выполнение
Метод step_through() принципиально меняет способ выполнения конвейера. Вместо последовательного выполнения всех шагов и возврата только итогового результата он превращает конвейер в итератор, который возвращает промежуточные результаты.
Вот что происходит внутри: метод начинает с преобразования входных данных во внутренний формат переходов конвейера, после чего возвращает это начальное состояние. Затем он применяет первый шаг обработки и возвращает результат. Далее он применяет второй шаг к этому результату и снова возвращает значение, и так далее. Каждый yield предоставляет вам полный снимок перехода на данный момент.
Этот генераторный паттерн мощен, поскольку он ленивый — пайплайн вычисляет следующий шаг только тогда, когда вы его запрашиваете. Это означает, что вы можете остановиться в любой момент, тщательно изучить текущее состояние и решить, продолжать ли работу. Вам не нужно запускать весь пайплайн, чтобы отладить один проблемный шаг.
Вместо запуска всего конвейера и просмотра только итогового результата, step_through() приостанавливается после каждого шага и показывает промежуточный переход:
# 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}")Интерактивная отладка с использованием точек останова
Вы можете добавлять точки останова в цикл пошагового выполнения для интерактивной отладки:
# 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В ходе сессии отладчика вы можете:
- Осмотрите
intermediate[TransitionKey.OBSERVATION], чтобы увидеть данные наблюдений - Проверьте
intermediate[TransitionKey.ACTION]на предмет преобразований действий - Осмотрите любую часть перехода, чтобы понять, что делает каждый шаг
Пошаговая отладка идеально подходит для понимания преобразований данных, но что насчёт структуры этих данных? Хотя хуки и пошаговая отладка помогают отлаживать поведение во время выполнения, вам также необходимо убедиться, что ваш конвейер формирует данные в формате, ожидаемом нижестоящими компонентами. Именно здесь на помощь приходит валидация контракта признаков.
Проверка контрактов признаков
Контракты функций определяют структуру данных, которую ваш конвейер ожидает на входе и формирует на выходе. Проверка этих контрактов помогает выявлять несоответствия на ранней стадии.
Понимание контрактов признаков
Каждый шаг процессора имеет метод transform_features(), который описывает, как он изменяет структуру данных:
# 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}")Проверка ожидаемых функций
Проверьте, что ваш конвейер формирует ожидаемые вами признаки:
# 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}")Эта проверка помогает убедиться, что ваш конвейер будет корректно работать с последующими компонентами, ожидающими определённые структуры данных.
Краткое описание
Теперь, когда вы понимаете три подхода к отладке, вы можете систематически решать любые проблемы в конвейере:
- Хуки — для мониторинга и проверки во время выполнения без изменения кода конвейера
- Пошаговый режим — для проверки промежуточных состояний и понимания преобразований
- Проверка функций — для обеспечения соответствия контрактам структуры данных
Когда использовать каждый подход:
- Начните с пошаговой отладки, когда вам нужно понять, что делает ваш конвейер, или когда происходит что-то неожиданное
- Добавьте хуки для непрерывного мониторинга на этапах разработки и в продакшене, чтобы автоматически выявлять проблемы
- Используйте проверку признаков перед развёртыванием, чтобы убедиться, что ваш конвейер корректно работает с последующими компонентами
Эти три инструмента работают в связке, обеспечивая полную наблюдаемость, которой по своей природе лишены сложные конвейеры. С хуками, отслеживающими проблемы, пошаговым отладчиком, помогающим понять поведение, и валидацией признаков, гарантирующей совместимость, вы сможете уверенно и эффективно отлаживать любой конвейер.
Update on GitHub