LeRobot documentation
프로세서 파이프라인 디버깅
프로세서 파이프라인 디버깅
Processor 파이프라인은 특히 여러 변환 단계를 연결하는 경우 복잡할 수 있습니다. 단순한 함수 호출과 달리 파이프라인에는 자연스러운 관찰 가능성이 부족하여 각 단계 사이에 어떤 일이 일어나는지 또는 어디서 문제가 발생하는지 쉽게 파악할 수 없습니다. 이 가이드는 이러한 문제를 해결하고 파이프라인을 통한 데이터 흐름을 이해하는 데 도움이 되도록 특별히 설계된 디버깅 도구와 기법을 제공합니다.
세 가지 상호 보완적인 디버깅 접근법을 살펴보겠습니다: 런타임 모니터링을 위한 hooks, 상세한 검사를 위한 step-through debugging, 그리고 구조적 불일치를 포착하기 위한 feature validation입니다. 각 방법은 서로 다른 목적을 수행하며, 함께 사용되면 파이프라인의 동작에 대한 완전한 가시성을 제공합니다.
훅 이해하기
Hooks는 파이프라인 실행 중 특정 시점에 호출되는 함수입니다. 파이프라인 코드를 변경하지 않고 데이터를 검사, 모니터링 또는 수정할 수 있는 방법을 제공합니다. 파이프라인의 “이벤트 리스너”라고 생각하면 됩니다.
Hook이란 무엇인가요?
hook은 파이프라인 실행 중 특정 시점에 자동으로 호출되는 콜백 함수입니다. 이 개념은 이벤트 기반 프로그래밍에서 유래했으며, 파이프라인의 실행 흐름에 “hook”을 걸어 현재 일어나는 일을 관찰하거나 반응할 수 있다고 상상해 보세요.
Hooks를 파이프라인에 체크포인트를 삽입하는 것으로 생각할 수 있습니다. 파이프라인이 이러한 체크포인트 중 하나에 도달할 때마다 잠시 멈추어 사용자 정의 hook 함수를 호출하므로, 현재 상태를 검사하고 정보를 기록하며 데이터를 검증할 수 있는 기회를 제공합니다.
는 두 개의 매개변수를 받는 함수입니다:
step_idx: int- 현재 처리 단계의 인덱스 (0, 1, 2 등)transition: EnvTransition- 파이프라인의 해당 지점에서의 데이터 전환
Hooks의 장점은 비침습적(non-invasive)이라는 점입니다. 파이프라인 코드의 한 줄도 변경하지 않고 모니터링, 검증, 디버깅 로직을 추가할 수 있습니다. 파이프라인은 핵심 로직에 집중된 깔끔한 상태를 유지하는 동안, hooks는 로깅, 모니터링, 디버깅과 같은 횡단 관심사(cross-cutting concerns)를 처리합니다.
Before vs After Hooks
파이프라인은 두 가지 유형의 훅을 지원합니다:
- Before 훅 (
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-step 훅과 after-step 훅을 위한 두 개의 별도 목록을 유지합니다. 훅을 등록하면 해당 목록에 단순히 추가됩니다.
실행 중 파이프라인은 엄격한 순서를 따릅니다. 각 처리 단계마다 먼저 등록 순서대로 모든 before-hook을 호출하고, 실제 단계 변환을 실행한 후, 등록 순서대로 모든 after-hook을 호출합니다. 이를 통해 각 단계 주위에 예측 가능한 샌드위치 형태의 구조가 형성됩니다.
핵심 통찰은 훅이 핵심 파이프라인 로직을 변경하지 않는다는 점입니다. 이는 순수하게 추가적인 요소입니다. 파이프라인의 _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)hooks는 특정 문제(예: NaN 감지)를 모니터링하거나 정상적인 파이프라인 실행 중에 메트릭을 수집하는 데 매우 유용하지만, 때로는 더 깊이 파고들어야 할 필요가 있습니다. 각 단계에서 정확히 어떤 일이 일어나는지 이해하거나 복잡한 변환 로직을 디버깅해야 하는 경우, 단계별 디버깅(step-through debugging)이 필요한 상세한 검사를 제공합니다.
단계별 디버깅
Step-through debugging은 파이프라인에 대한 슬로 모션 리플레이와 같습니다. 입력에서 출력으로 데이터가 한 번에 빠르게 변환되는 것을 지켜보는 대신, 각 개별 단계 이후에 어떤 일이 일어나는지 일시 정지하고 살펴볼 수 있습니다.
이 접근법은 복잡한 파이프라인을 이해하거나, 예상치 못한 동작을 디버깅하거나, 각 변환이 의도대로 작동하는지 확인하려는 경우에 특히 유용합니다. 자동화된 모니터링에 탁월한 훅과 달리, 단계별 디버깅은 검사 과정에 대한 수동적이고 인터랙티브한 제어를 제공합니다.
step_through() 메서드는 각 처리 단계 이후의 전이 상태를 반환하는 생성자(generator)로, 중간 결과를 검사할 수 있습니다. 파이프라인을 통해 데이터가 흐르는 동안 일련의 스냅샷을 생성한다고 생각하면 됩니다. 각 스냅샷은 변환이 한 번 더 적용된 후의 데이터 상태를 정확히 보여줍니다.
Step-Through 작동 방식
step_through() 메서드는 파이프라인의 실행 방식을 근본적으로 변경합니다. 모든 단계를 순차적으로 실행한 후 최종 결과만 반환하는 대신, 파이프라인을 중간 결과를 반환하는 반복자로 변환합니다.
내부적으로 다음과 같은 과정이 진행됩니다: 메서드는 입력 데이터를 파이프라인의 내부 전이 형식으로 변환한 후, 이 초기 상태를 반환합니다. 다음으로, 첫 번째 처리 단계를 적용하고 그 결과를 반환합니다. 그런 다음 해당 결과에 두 번째 단계를 적용하고 다시 반환하며, 이 과정이 반복됩니다. 각 yield는 해당 시점의 전이에 대한 완전한 스냅샷을 제공합니다.
이 생성기 패턴은 지연 평가(lazy) 방식이라 강력합니다. 파이프라인은 요청할 때만 다음 단계를 계산합니다. 이는 언제든 중단하고 현재 상태를 면밀히 검사한 후 계속 진행할지 결정할 수 있음을 의미합니다. 문제 있는 단계를 디버깅하기 위해 파이프라인 전체를 실행할 필요가 없습니다.
전체 파이프라인을 실행한 후 최종 결과만 확인하는 대신, 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}")Interactive Debugging with Breakpoints
단계별 루프에 브레이크포인트를 추가하여 인터랙티브 디버깅을 수행할 수 있습니다:
# 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]을 확인하세요 - 전이 과정의 각 단계를 검사하여 각 단계가 수행하는 작업을 이해합니다
Step-through debugging은 data 변환을 이해하는 데 적합하지만, 해당 데이터의 structure는 어떻게 해야 할까요? 훅과 step-through는 런타임 동작 디버깅에 도움이 되지만, 파이프라인이 다운스트림 구성 요소가 기대하는 형식으로 데이터를 생성하는지 확인하는 것도 필요합니다. 바로 이 지점에서 feature contract validation이 필요합니다.
기능 계약 검증
Feature contracts는 파이프라인이 입력으로 기대하고 출력으로 생성하는 데이터 구조를 정의합니다. 이러한 계약의 검증은 불일치를 조기에 발견하는 데 도움이 됩니다.
피처 계약 이해하기
각 프로세서 단계에는 데이터 구조를 어떻게 변경하는지 설명하는 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}")이 검증은 특정 데이터 구조를 기대하는 하위 구성 요소와 파이프라인이 올바르게 작동하도록 하는 데 도움이 됩니다.
요약
세 가지 디버깅 접근법을 이해했으므로 이제 모든 파이프라인 문제를 체계적으로 해결할 수 있습니다:
- Hooks - 파이프라인 코드를 수정하지 않고 런타임 모니터링 및 검증을 수행
- 단계별 실행 - 중간 상태를 검사하고 변환 과정을 이해하는 데 사용
- 기능 검증 - 데이터 구조 계약이 충족되는지 확인하기 위해
각 접근법을 사용할 때:
- 파이프라인의 동작을 이해해야 하거나 예상치 못한 상황이 발생했을 때 단계별 디버깅부터 시작하세요
- 개발 및 프로덕션 환경에서 문제를 자동으로 감지하기 위해 hooks를 추가하여 지속적으로 모니터링합니다
- 배포 전에 feature validation을 사용하여 파이프라인이 다운스트림 구성 요소와 함께 작동하는지 확인하세요
이 세 가지 도구가 함께 작동하여 복잡한 파이프라인에 자연스럽게 부족한 완전한 관측 가능성을 제공합니다. 문제를 감시하는 훅, 동작을 이해하는 데 도움을 주는 스텝스루, 호환성을 보장하는 기능 검증 덕분에 모든 파이프라인을 자신 있고 효율적으로 디버깅할 수 있습니다.
Update on GitHub