LeRobot documentation

プロセッサのパイプラインをデバッグする

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

プロセッサのパイプラインをデバッグする

Processorのパイプラインは複雑になりがちで、特に複数の変換ステップをチェーンする場合に顕著です。 単純な関数呼び出しとは異なり、パイプラインには自然な観測可能性がなく、各ステップの間に何が起きているのか、あるいはどこで問題が発生しているのかを簡単に把握することができません。 このガイドでは、これらの課題に対処し、パイプライン内でのデータフローを理解するために特化して設計されたデバッグツールとテクニックを提供します。

3つの補完的なデバッグアプローチを解説します: ランタイム監視のためのフック、詳細な検査のためのステップスルーデバッグ、構造の不一致を検出するための機能検証です。それぞれが異なる目的を担い、これらを組み合わせることで、パイプラインの動作に対する完全な可視化を実現できます。

フックの理解

Hooksは、パイプラインの実行中に特定のタイミングで呼び出される関数です。 パイプラインのコードを変更せずに、データを検査、監視、または変更する方法を提供します。 パイプラインの「イベントリスナー」として考えてください。

フックとは何ですか?

フックとは、パイプラインの実行中に特定のタイミングで自動的に呼び出されるコールバック関数です。 この概念はイベント駆動型プログラミングに由来します。パイプラインの実行フローに「フック」を仕込み、そこで何が起きているかを観察したり、反応したりできると想像してください。

フックは、パイプラインにチェックポイントを挿入するものだと考えてください。パイプラインがこれらのチェックポイントのいずれかに到達するたびに、フック関数を呼び出すために一時的に停止し、現在の状態を確認し、情報をログに記録し、データを検証する機会が得られます。

フックとは、2つのパラメータを受け取る関数のことです:

  • step_idx: int - 現在の処理ステップのインデックス(0、1、2 など)
  • transition: EnvTransition - パイプラインのその時点でのデータ遷移

フックの美点は、その非侵入性にあります。パイプラインのコードを1行も変更せずに、監視、検証、デバッグのロジックを追加できます。パイプラインはコアロジックに集中したクリーンな状態を保ち、ログ記録、監視、デバッグといった横断的な関心事はフックが処理します。

Before vs After Hooks

パイプラインは2種類のフックをサポートしています:

  • 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フック用の2つの独立したリストを保持しています。フックを登録すると、適切なリストの末尾に追加されるだけです。

実行中、パイプラインは厳密な順序に従います。各処理ステップについて、まず登録順にすべての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() メソッドはジェネレーターであり、各処理ステップの後に遷移状態を生成します。これにより、中間結果を調べることができます。パイプラインを流れるデータのスナップショットの系列を作成していると考えてください。各スナップショットは、さらに1つの変換が適用された後のデータの正確な状態を示します。

ステップスルーの仕組み

step_through() メソッドは、パイプラインの実行方法を実質的に変更します。すべてのステップを順次実行して最終結果のみを返す代わりに、パイプラインをイテレータに変換し、中間結果を生成します。

内部では以下のように動作します。メソッドは、まず入力データをパイプラインの内部遷移形式に変換し、この初期状態を生成します。次に、最初の処理ステップを適用し、その結果を生成します。その後、その結果に対して2番目のステップを適用して再び生成し、このように続きます。各yieldは、その時点での遷移の完全なスナップショットを返します。

このジェネレーターパターンは強力です。その理由は、遅延評価(lazy)であるため、パイプラインは次のステップを要求したときにのみ計算を行うからです。これにより、任意の時点で処理を停止し、現在の状態を徹底的に確認したうえで、継続するかどうかを判断できます。問題のある1つのステップをデバッグするために、パイプライン全体を実行する必要はありません。

パイプライン全体を実行して最終結果のみを確認するのではなく、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]を確認してください
  • 遷移の任意の部分を調査して、各ステップが何を行うかを理解します

ステップスルーデバッグはデータの変換を理解するには最適ですが、そのデータの構造についてはどうでしょうか。フックやステップスルーは実行時の動作のデバッグに役立ちますが、パイプラインが下流のコンポーネントが期待する形式でデータを生成していることも確認する必要があります。ここで登場するのが、フィーチャー契約の検証です。

機能契約の検証

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

この検証により、特定のデータ構造を前提とする下流のコンポーネントとパイプラインが正しく動作することが保証されます。

概要

3つのデバッグアプローチを理解したことで、パイプラインの問題を体系的に解決できるようになりました:

  1. フック - パイプラインコードを変更せずにランタイムの監視と検証を行う
  2. ステップスルー - 中間状態の検査と変換の理解に
  3. 機能の検証 - データ構造の契約が満たされていることを保証するために

各アプローチの適用タイミング:

  • パイプラインの動作を理解する必要がある場合や、予期しないことが起きた場合は、ステップスルーデバッグから始めます
  • 開発および本番環境での継続的な監視のためにフックを追加し、問題を自動的に検出します
  • デプロイ前に機能検証を実行し、パイプラインが下流のコンポーネントと正常に動作することを確認してください

これら3つのツールは連携して動作し、複雑なパイプラインに自然と欠ける完全な観測可能性(オブザーバビリティ)を提供します。フックが問題を監視し、ステップスルーが動作の理解を助け、機能検証が互換性を保証することで、あらゆるパイプラインを自信を持って効率的にデバッグできるようになります。

Update on GitHub