LeRobot documentation

프로세서 파이프라인 디버깅

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

프로세서 파이프라인 디버깅

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}")

이 검증은 특정 데이터 구조를 기대하는 하위 구성 요소와 파이프라인이 올바르게 작동하도록 하는 데 도움이 됩니다.

요약

세 가지 디버깅 접근법을 이해했으므로 이제 모든 파이프라인 문제를 체계적으로 해결할 수 있습니다:

  1. Hooks - 파이프라인 코드를 수정하지 않고 런타임 모니터링 및 검증을 수행
  2. 단계별 실행 - 중간 상태를 검사하고 변환 과정을 이해하는 데 사용
  3. 기능 검증 - 데이터 구조 계약이 충족되는지 확인하기 위해

각 접근법을 사용할 때:

  • 파이프라인의 동작을 이해해야 하거나 예상치 못한 상황이 발생했을 때 단계별 디버깅부터 시작하세요
  • 개발 및 프로덕션 환경에서 문제를 자동으로 감지하기 위해 hooks를 추가하여 지속적으로 모니터링합니다
  • 배포 전에 feature validation을 사용하여 파이프라인이 다운스트림 구성 요소와 함께 작동하는지 확인하세요

이 세 가지 도구가 함께 작동하여 복잡한 파이프라인에 자연스럽게 부족한 완전한 관측 가능성을 제공합니다. 문제를 감시하는 훅, 동작을 이해하는 데 도움을 주는 스텝스루, 호환성을 보장하는 기능 검증 덕분에 모든 파이프라인을 자신 있고 효율적으로 디버깅할 수 있습니다.

Update on GitHub