* renaming to episodic

* changing semantics of the docstring
* fixing leader - follower handover disable torque
* adding optionnal config to disable handover
This commit is contained in:
Maximellerbach
2026-06-05 11:45:58 +02:00
parent eb21cd2b26
commit 8b1ffbe5c8
10 changed files with 58 additions and 51 deletions
+1 -1
View File
@@ -647,6 +647,6 @@ The `--strategy.type` flag selects the execution mode:
- `sentry`: Continuous recording with auto-upload (useful for large-scale evaluation) - `sentry`: Continuous recording with auto-upload (useful for large-scale evaluation)
- `highlight`: Ring buffer recording with keystroke save (useful for capturing interesting events) - `highlight`: Ring buffer recording with keystroke save (useful for capturing interesting events)
- `dagger`: Human-in-the-loop data collection (see [HIL Data Collection](./hil_data_collection)) - `dagger`: Human-in-the-loop data collection (see [HIL Data Collection](./hil_data_collection))
- `legacy`: Episode-oriented policy recording with reset phases, mirrors the old `lerobot-record` inference path - `episodic`: Episode-oriented policy recording with reset phases between episodes
All strategies support `--inference.type=rtc` for smooth execution with slow VLA models (Pi0, Pi0.5, SmolVLA). All strategies support `--inference.type=rtc` for smooth execution with slow VLA models (Pi0, Pi0.5, SmolVLA).
+11 -9
View File
@@ -157,13 +157,13 @@ Foot pedal input is also supported via `--strategy.input_device=pedal`. Configur
| `--strategy.input_device` | Input device: `keyboard` or `pedal` (default: keyboard) | | `--strategy.input_device` | Input device: `keyboard` or `pedal` (default: keyboard) |
| `--teleop.type` | **Required.** Teleoperator type | | `--teleop.type` | **Required.** Teleoperator type |
### Legacy (`--strategy.type=legacy`) ### Episodic (`--strategy.type=episodic`)
Episode-oriented recording that mirrors the old `lerobot-record` inference path. The policy drives the robot for each episode; an optional teleoperator can drive the robot during the reset phase between episodes. Episode-oriented recording that mirrors the behavior of `lerobot-record`. The policy drives the robot for each episode; an optional teleoperator can drive the robot during the reset phase between episodes.
```bash ```bash
lerobot-rollout \ lerobot-rollout \
--strategy.type=legacy \ --strategy.type=episodic \
--policy.path=${HF_USER}/my_policy \ --policy.path=${HF_USER}/my_policy \
--robot.type=so100_follower \ --robot.type=so100_follower \
--robot.port=/dev/ttyACM0 \ --robot.port=/dev/ttyACM0 \
@@ -186,12 +186,14 @@ Teleop is optional — if omitted the robot holds its position during the reset
| `←` (left) | Discard episode and re-record it | | `←` (left) | Discard episode and re-record it |
| `ESC` | Stop the recording session | | `ESC` | Stop the recording session |
| Flag | Description | | Flag | Description |
| -------------------------- | ------------------------------------------------------- | | -------------------------------------------- | -------------------------------------------------------------------------- |
| `--dataset.num_episodes` | Number of episodes to record | | `--dataset.num_episodes` | Number of episodes to record |
| `--dataset.episode_time_s` | Duration of each recording episode in seconds | | `--dataset.episode_time_s` | Duration of each recording episode in seconds |
| `--dataset.reset_time_s` | Duration of the reset phase between episodes in seconds | | `--dataset.reset_time_s` | Duration of the reset phase between episodes in seconds |
| `--teleop.type` | Optional. Teleoperator to drive the robot during resets | | `--teleop.type` | Optional. Teleoperator to drive the robot during resets |
| `--strategy.reset_to_initial_position` | Whether to reset the robot to its initial position between episodes |
| `--strategy.smooth_leader_follower_handover` | Whether to turn on or off the leader -> follower smooth handover behavior. |
--- ---
+2
View File
@@ -248,6 +248,8 @@ def sanity_check_dataset_robot_compatibility(
######################################################################################## ########################################################################################
# Teleoperator smooth handover helpers # Teleoperator smooth handover helpers
# NOTE(Maxime): These functions use minimal type hints to maintain compatibility with utils
# being a root module.
######################################################################################## ########################################################################################
+4 -4
View File
@@ -23,8 +23,8 @@ from .configs import (
DAggerKeyboardConfig, DAggerKeyboardConfig,
DAggerPedalConfig, DAggerPedalConfig,
DAggerStrategyConfig, DAggerStrategyConfig,
EpisodicStrategyConfig,
HighlightStrategyConfig, HighlightStrategyConfig,
LegacyStrategyConfig,
RolloutConfig, RolloutConfig,
RolloutStrategyConfig, RolloutStrategyConfig,
SentryStrategyConfig, SentryStrategyConfig,
@@ -50,8 +50,8 @@ from .inference import (
from .strategies import ( from .strategies import (
BaseStrategy, BaseStrategy,
DAggerStrategy, DAggerStrategy,
EpisodicStrategy,
HighlightStrategy, HighlightStrategy,
LegacyStrategy,
RolloutStrategy, RolloutStrategy,
SentryStrategy, SentryStrategy,
create_strategy, create_strategy,
@@ -68,8 +68,8 @@ __all__ = [
"HardwareContext", "HardwareContext",
"HighlightStrategy", "HighlightStrategy",
"HighlightStrategyConfig", "HighlightStrategyConfig",
"LegacyStrategy", "EpisodicStrategy",
"LegacyStrategyConfig", "EpisodicStrategyConfig",
"InferenceEngine", "InferenceEngine",
"InferenceEngineConfig", "InferenceEngineConfig",
"PolicyContext", "PolicyContext",
+11 -7
View File
@@ -121,16 +121,16 @@ class DAggerPedalConfig:
upload: str = "KEY_C" upload: str = "KEY_C"
@RolloutStrategyConfig.register_subclass("legacy") @RolloutStrategyConfig.register_subclass("episodic")
@dataclass @dataclass
class LegacyStrategyConfig(RolloutStrategyConfig): class EpisodicStrategyConfig(RolloutStrategyConfig):
"""Episode-oriented recording that mirrors the pre-rollout lerobot-record behavior. """Episode-oriented recording that mirrors the behavior of ``lerobot-record``.
Records ``dataset.num_episodes`` episodes of maximum ``dataset.episode_time_s`` each. Records ``dataset.num_episodes`` episodes of maximum ``dataset.episode_time_s`` each.
After each episode, runs ``dataset.reset_time_s`` seconds of reset time. After each episode, runs ``dataset.reset_time_s`` seconds of reset time.
Keyboard controls (same as lerobot-record): Keyboard controls:
Right arrow end episode early Right arrow end current episode or reset phase early
Left arrow discard current episode and re-record Left arrow discard current episode and re-record
Escape stop recording session Escape stop recording session
@@ -143,7 +143,11 @@ class LegacyStrategyConfig(RolloutStrategyConfig):
# When True (default), moves the robot back to the joint positions captured at startup. # When True (default), moves the robot back to the joint positions captured at startup.
# Otherwise, leave the robot in its current position. # Otherwise, leave the robot in its current position.
reset_to_initial_position: bool = True reset_to_initial_position: bool = True
pass
# Whether to turn on or off the leader -> follower smooth handover behavior.
# When False, fallback to follower -> leader handover.
# Note that leader -> follower handover is only supported when the leader has `send_feedback` capability.
smooth_leader_follower_handover: bool = True
@RolloutStrategyConfig.register_subclass("dagger") @RolloutStrategyConfig.register_subclass("dagger")
@@ -259,7 +263,7 @@ class RolloutConfig:
SentryStrategyConfig, SentryStrategyConfig,
HighlightStrategyConfig, HighlightStrategyConfig,
DAggerStrategyConfig, DAggerStrategyConfig,
LegacyStrategyConfig, EpisodicStrategyConfig,
), ),
) )
if needs_dataset and (self.dataset is None or not self.dataset.repo_id): if needs_dataset and (self.dataset is None or not self.dataset.repo_id):
+2 -2
View File
@@ -17,9 +17,9 @@
from .base import BaseStrategy from .base import BaseStrategy
from .core import RolloutStrategy, estimate_max_episode_seconds, safe_push_to_hub, send_next_action from .core import RolloutStrategy, estimate_max_episode_seconds, safe_push_to_hub, send_next_action
from .dagger import DAggerEvents, DAggerPhase, DAggerStrategy from .dagger import DAggerEvents, DAggerPhase, DAggerStrategy
from .episodic import EpisodicStrategy
from .factory import create_strategy from .factory import create_strategy
from .highlight import HighlightStrategy from .highlight import HighlightStrategy
from .legacy import LegacyStrategy
from .sentry import SentryStrategy from .sentry import SentryStrategy
__all__ = [ __all__ = [
@@ -28,7 +28,7 @@ __all__ = [
"DAggerPhase", "DAggerPhase",
"DAggerStrategy", "DAggerStrategy",
"HighlightStrategy", "HighlightStrategy",
"LegacyStrategy", "EpisodicStrategy",
"RolloutStrategy", "RolloutStrategy",
"SentryStrategy", "SentryStrategy",
"create_strategy", "create_strategy",
@@ -12,17 +12,15 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Legacy rollout strategy: policy-driven multi-episode recording with reset phases. """Episodic rollout strategy: mirrors the behavior of ``lerobot-record``.
Mirrors the pre-rollout ``lerobot-record`` inference path:
- Policy drives the robot during each recording episode. - Policy drives the robot during each recording episode.
- An optional teleoperator can drive the robot during reset phases so the - An optional teleoperator can drive the robot during reset phases so the
operator can bring the environment back to its starting configuration. operator can bring the environment back to its starting configuration.
If no teleop is connected the robot stays in its current position. If no teleop is connected the robot stays in its current position.
- Keyboard controls (identical to the old lerobot-record): - Keyboard controls:
Right arrow end the current episode early Right arrow end the current episode or reset phase early
Left arrow discard the current episode and re-record it Left arrow discard the current episode and re-record it
Escape stop the recording session Escape stop the recording session
@@ -49,15 +47,15 @@ from lerobot.utils.robot_utils import precise_sleep
from lerobot.utils.utils import log_say from lerobot.utils.utils import log_say
from lerobot.utils.visualization_utils import log_rerun_data from lerobot.utils.visualization_utils import log_rerun_data
from ..configs import LegacyStrategyConfig from ..configs import EpisodicStrategyConfig
from ..context import RolloutContext from ..context import RolloutContext
from .core import RolloutStrategy, safe_push_to_hub, send_next_action from .core import RolloutStrategy, safe_push_to_hub, send_next_action
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class LegacyStrategy(RolloutStrategy): class EpisodicStrategy(RolloutStrategy):
"""Policy-driven multi-episode recording (mirrors old lerobot-record inference path). """Policy-driven multi-episode recording, mirrors the behavior of ``lerobot-record``.
Each recording episode runs the policy for maximum ``dataset.episode_time_s`` Each recording episode runs the policy for maximum ``dataset.episode_time_s``
seconds, recording every frame. A reset phase of ``dataset.reset_time_s`` seconds, recording every frame. A reset phase of ``dataset.reset_time_s``
@@ -69,14 +67,14 @@ class LegacyStrategy(RolloutStrategy):
the start of each recording episode. the start of each recording episode.
Keyboard events: Keyboard events:
right arrow exit current episode early right arrow end current episode or reset phase early
left arrow discard & re-record current episode left arrow discard & re-record current episode
ESC stop the session ESC stop the session
""" """
config: LegacyStrategyConfig config: EpisodicStrategyConfig
def __init__(self, config: LegacyStrategyConfig) -> None: def __init__(self, config: EpisodicStrategyConfig) -> None:
super().__init__(config) super().__init__(config)
self._listener = None self._listener = None
self._events: dict | None = None self._events: dict | None = None
@@ -85,7 +83,7 @@ class LegacyStrategy(RolloutStrategy):
"""Start the inference engine and attach the keyboard listener.""" """Start the inference engine and attach the keyboard listener."""
self._init_engine(ctx) self._init_engine(ctx)
self._listener, self._events = init_keyboard_listener() self._listener, self._events = init_keyboard_listener()
logger.info("Legacy strategy ready") logger.info("Episodic strategy ready")
def run(self, ctx: RolloutContext) -> None: def run(self, ctx: RolloutContext) -> None:
"""Main multi-episode recording loop.""" """Main multi-episode recording loop."""
@@ -150,7 +148,8 @@ class LegacyStrategy(RolloutStrategy):
current_pos = {k: v for k, v in obs.items() if k.endswith(".pos")} current_pos = {k: v for k, v in obs.items() if k.endswith(".pos")}
if teleop_supports_feedback(teleop): if teleop_supports_feedback(teleop):
logger.info("Smooth handover: moving leader arm to follower position") logger.info("Smooth handover: moving leader arm to follower position")
teleop_smooth_move_to(teleop, current_pos, duration_s=1) teleop_smooth_move_to(teleop, current_pos, duration_s=2)
teleop.disable_torque()
else: else:
logger.info("Smooth handover: sliding follower to teleop position") logger.info("Smooth handover: sliding follower to teleop position")
teleop_action = teleop.get_action() teleop_action = teleop.get_action()
@@ -191,7 +190,7 @@ class LegacyStrategy(RolloutStrategy):
# Save any frames buffered in the current episode so an unexpected # Save any frames buffered in the current episode so an unexpected
# exception or KeyboardInterrupt does not silently drop recorded data. # exception or KeyboardInterrupt does not silently drop recorded data.
# suppress: save_episode raises if the buffer is empty (nothing to lose). # suppress: save_episode raises if the buffer is empty (nothing to lose).
logger.info("Legacy control loop ended — saving any in-progress episode") logger.info("Episodic control loop ended — saving any in-progress episode")
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
dataset.save_episode() dataset.save_episode()
@@ -330,4 +329,4 @@ class LegacyStrategy(RolloutStrategy):
return_to_initial_position=cfg.return_to_initial_position, return_to_initial_position=cfg.return_to_initial_position,
) )
log_say("Exiting", play_sounds) log_say("Exiting", play_sounds)
logger.info("Legacy strategy teardown complete") logger.info("Episodic strategy teardown complete")
+4 -4
View File
@@ -21,8 +21,8 @@ from typing import TYPE_CHECKING
from .base import BaseStrategy from .base import BaseStrategy
from .core import RolloutStrategy from .core import RolloutStrategy
from .dagger import DAggerStrategy from .dagger import DAggerStrategy
from .episodic import EpisodicStrategy
from .highlight import HighlightStrategy from .highlight import HighlightStrategy
from .legacy import LegacyStrategy
from .sentry import SentryStrategy from .sentry import SentryStrategy
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -43,8 +43,8 @@ def create_strategy(config: RolloutStrategyConfig) -> RolloutStrategy:
return HighlightStrategy(config) return HighlightStrategy(config)
if config.type == "dagger": if config.type == "dagger":
return DAggerStrategy(config) return DAggerStrategy(config)
if config.type == "legacy": if config.type == "episodic":
return LegacyStrategy(config) return EpisodicStrategy(config)
raise ValueError( raise ValueError(
f"Unknown strategy type '{config.type}'. Available: base, sentry, highlight, dagger, legacy" f"Unknown strategy type '{config.type}'. Available: base, sentry, highlight, dagger, episodic"
) )
+4 -4
View File
@@ -25,7 +25,7 @@ Strategies
--strategy.type=sentry Continuous recording with auto-upload --strategy.type=sentry Continuous recording with auto-upload
--strategy.type=highlight Ring buffer + keystroke save --strategy.type=highlight Ring buffer + keystroke save
--strategy.type=dagger Human-in-the-loop (DAgger / RaC) --strategy.type=dagger Human-in-the-loop (DAgger / RaC)
--strategy.type=legacy Episode oriented recording, mirrors old lerobot-record --strategy.type=episodic Episode-oriented recording with reset phases
Inference backends Inference backends
------------------ ------------------
@@ -112,15 +112,15 @@ Usage examples
--display_data=true \\ --display_data=true \\
--use_torch_compile=true --use_torch_compile=true
# Legacy mode — episode-oriented recording, mirrors old lerobot-record # Episodic mode — episode-oriented recording with reset phases
lerobot-rollout \\ lerobot-rollout \\
--strategy.type=legacy \\ --strategy.type=episodic \\
--policy.path=user/my_policy \\ --policy.path=user/my_policy \\
--robot.type=so100_follower \\ --robot.type=so100_follower \\
--robot.port=/dev/ttyACM0 \\ --robot.port=/dev/ttyACM0 \\
--teleop.type=so100_leader \\ --teleop.type=so100_leader \\
--teleop.port=/dev/ttyACM1 \\ --teleop.port=/dev/ttyACM1 \\
--dataset.repo_id=user/rollout_legacy_data \\ --dataset.repo_id=user/rollout_episodic_data \\
--dataset.num_episodes=20 \\ --dataset.num_episodes=20 \\
--dataset.single_task="Grab the cube" --dataset.single_task="Grab the cube"
+5 -5
View File
@@ -59,8 +59,8 @@ def test_strategy_config_types():
from lerobot.rollout import ( from lerobot.rollout import (
BaseStrategyConfig, BaseStrategyConfig,
DAggerStrategyConfig, DAggerStrategyConfig,
EpisodicStrategyConfig,
HighlightStrategyConfig, HighlightStrategyConfig,
LegacyStrategyConfig,
SentryStrategyConfig, SentryStrategyConfig,
) )
@@ -68,7 +68,7 @@ def test_strategy_config_types():
assert SentryStrategyConfig().type == "sentry" assert SentryStrategyConfig().type == "sentry"
assert HighlightStrategyConfig().type == "highlight" assert HighlightStrategyConfig().type == "highlight"
assert DAggerStrategyConfig().type == "dagger" assert DAggerStrategyConfig().type == "dagger"
assert LegacyStrategyConfig().type == "legacy" assert EpisodicStrategyConfig().type == "episodic"
def test_dagger_config_invalid_input_device(): def test_dagger_config_invalid_input_device():
@@ -205,8 +205,8 @@ def test_create_strategy_dispatches():
BaseStrategyConfig, BaseStrategyConfig,
DAggerStrategy, DAggerStrategy,
DAggerStrategyConfig, DAggerStrategyConfig,
LegacyStrategy, EpisodicStrategy,
LegacyStrategyConfig, EpisodicStrategyConfig,
SentryStrategy, SentryStrategy,
SentryStrategyConfig, SentryStrategyConfig,
create_strategy, create_strategy,
@@ -215,7 +215,7 @@ def test_create_strategy_dispatches():
assert isinstance(create_strategy(BaseStrategyConfig()), BaseStrategy) assert isinstance(create_strategy(BaseStrategyConfig()), BaseStrategy)
assert isinstance(create_strategy(SentryStrategyConfig()), SentryStrategy) assert isinstance(create_strategy(SentryStrategyConfig()), SentryStrategy)
assert isinstance(create_strategy(DAggerStrategyConfig()), DAggerStrategy) assert isinstance(create_strategy(DAggerStrategyConfig()), DAggerStrategy)
assert isinstance(create_strategy(LegacyStrategyConfig()), LegacyStrategy) assert isinstance(create_strategy(EpisodicStrategyConfig()), EpisodicStrategy)
def test_create_strategy_unknown_raises(): def test_create_strategy_unknown_raises():