LeRobot documentation

Policy Deployment (lerobot-rollout)

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v0.6.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Policy Deployment (lerobot-rollout)

lerobot-rollout is the single CLI for deploying trained policies on real robots. It supports multiple execution strategies and inference backends, from quick evaluation to continuous recording and human-in-the-loop data collection.

Quick Start

No extra dependencies are needed beyond your robot and policy extras.

lerobot-rollout \
    --strategy.type=base \
    --policy.path=lerobot/act_koch_real \
    --robot.type=koch_follower \
    --robot.port=/dev/ttyACM0 \
    --task="pick up cube" \
    --duration=30

This runs the policy for 30 seconds with no recording.


Strategies

Select a strategy with --strategy.type=<name>. Each strategy defines a different control loop with its own recording and interaction semantics.

Base ( --strategy.type=base )

Autonomous policy execution with no data recording. Use this for quick evaluation, demos, or when you only need to observe the robot.

lerobot-rollout \
    --strategy.type=base \
    --policy.path=${HF_USER}/my_policy \
    --robot.type=so100_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
    --task="Put lego brick into the box" \
    --duration=60
FlagDescription
--durationRun time in seconds (0 = infinite)
--taskTask description passed to the policy
--display_dataStream observations/actions to Rerun for visualization

Sentry ( --strategy.type=sentry )

Continuous autonomous recording with periodic upload to the Hugging Face Hub. Episode boundaries are auto-computed from camera resolution and FPS so each saved episode produces a complete video file, keeping uploads efficient.

Policy state (hidden state, RTC queue) persists across episode boundaries: the robot does not reset between episodes.

lerobot-rollout \
    --strategy.type=sentry \
    --strategy.upload_every_n_episodes=5 \
    --policy.path=${HF_USER}/my_policy \
    --robot.type=so100_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
    --dataset.repo_id=${HF_USER}/rollout_eval_data \
    --dataset.single_task="Put lego brick into the box" \
    --duration=3600
FlagDescription
--strategy.upload_every_n_episodesPush to Hub every N episodes (default: 5)
--strategy.target_video_file_size_mbTarget video file size for episode rotation (default: auto)
--dataset.repo_idRequired. Hub repository for the recorded dataset
--dataset.push_to_hubWhether to push to Hub on teardown (default: true)

Highlight ( --strategy.type=highlight )

Autonomous rollout with on-demand recording via a memory-bounded ring buffer. The robot runs continuously while the buffer captures the last N seconds of telemetry. Press the save key to flush the buffer and start live recording; press it again to save the episode.

lerobot-rollout \
    --strategy.type=highlight \
    --strategy.ring_buffer_seconds=30 \
    --strategy.save_key=s \
    --strategy.push_key=h \
    --policy.path=${HF_USER}/my_policy \
    --robot.type=koch_follower \
    --robot.port=/dev/ttyACM0 \
    --dataset.repo_id=${HF_USER}/rollout_highlight_data \
    --dataset.single_task="Pick up the red cube"

Keyboard controls:

KeyAction
s (configurable)Start recording (flushes buffer) / stop and save episode
h (configurable)Push dataset to Hub
ESCStop the session
FlagDescription
--strategy.ring_buffer_secondsDuration of buffered telemetry (default: 30)
--strategy.ring_buffer_max_memory_mbMemory cap for the ring buffer (default: 2048)
--strategy.save_keyKey to toggle recording (default: s)
--strategy.push_keyKey to push to Hub (default: h)

DAgger ( --strategy.type=dagger )

Human-in-the-loop data collection. Alternates between autonomous policy execution and human intervention via a teleoperator. Intervention frames are tagged with intervention=True. Requires a teleoperator (--teleop.type).

See the Human-In-the-Loop Data Collection guide for a detailed walkthrough.

Corrections-only mode (default): Only human correction windows are recorded. Each correction becomes one episode.

lerobot-rollout \
    --strategy.type=dagger \
    --strategy.num_episodes=20 \
    --policy.path=outputs/pretrain/checkpoints/last/pretrained_model \
    --robot.type=bi_openarm_follower \
    --teleop.type=bi_openarm_mini \
    --dataset.repo_id=${HF_USER}/rollout_hil_data \
    --dataset.single_task="Fold the T-shirt"

Continuous recording mode (--strategy.record_autonomous=true): Both autonomous and correction frames are recorded with time-based episode rotation (same as Sentry).

lerobot-rollout \
    --strategy.type=dagger \
    --strategy.record_autonomous=true \
    --strategy.num_episodes=50 \
    --policy.path=${HF_USER}/my_policy \
    --robot.type=so100_follower \
    --robot.port=/dev/ttyACM0 \
    --teleop.type=so101_leader \
    --teleop.port=/dev/ttyACM1 \
    --dataset.repo_id=${HF_USER}/rollout_dagger_data \
    --dataset.single_task="Grasp the block"

Keyboard controls (default input device):

KeyAction
SpacePause / resume policy execution
TabStart / stop human correction
EnterPush dataset to Hub (corrections-only mode)
ESCStop the session

Foot pedal input is also supported via --strategy.input_device=pedal. Configure pedal codes with --strategy.pedal.* flags.

FlagDescription
--strategy.num_episodesNumber of correction episodes to record (default: 10)
--strategy.record_autonomousRecord autonomous frames too (default: false)
--strategy.upload_every_n_episodesPush to Hub every N episodes (default: 5)
--strategy.input_deviceInput device: keyboard or pedal (default: keyboard)
--strategy.smooth_handoverSmoothly hand control over at pause / correction start (default: true). Disable for clutch-style teleops that re-reference at the current robot pose on engage
--teleop.typeRequired. Teleoperator type

Episodic ( --strategy.type=episodic )

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.

lerobot-rollout \
    --strategy.type=episodic \
    --policy.path=${HF_USER}/my_policy \
    --robot.type=so100_follower \
    --robot.port=/dev/ttyACM0 \
    --teleop.type=so100_leader \
    --teleop.port=/dev/ttyACM1 \
    --dataset.repo_id=${HF_USER}/my_eval_data \
    --dataset.num_episodes=20 \
    --dataset.episode_time_s=30 \
    --dataset.reset_time_s=10 \
    --dataset.single_task="Pick up the red cube"

Teleop is optional — if omitted the robot holds its position during the reset phase.

Keyboard controls:

KeyAction
(right)End the current episode early
(left)Discard episode and re-record it
ESCStop the recording session
FlagDescription
--dataset.num_episodesNumber of episodes to record
--dataset.episode_time_sDuration of each recording episode in seconds
--dataset.reset_time_sDuration of the reset phase between episodes in seconds
--teleop.typeOptional. Teleoperator to drive the robot during resets
--strategy.reset_to_initial_positionWhether to reset the robot to its initial position between episodes
--strategy.smooth_leader_to_follower_handoverWhether to turn on or off the leader -> follower smooth handover behavior.
--strategy.smooth_handoverSmoothly hand control to the teleop at reset start (default: true). Disable for clutch-style teleops that re-reference at the current robot pose on engage

Inference Backends

Select a backend with --inference.type=<name>. All strategies work with both backends.

Sync (default)

One policy call per control tick. The main loop blocks until the action is computed.

Works with all policies. No extra flags needed.

Real-Time Chunking ( --inference.type=rtc )

A background thread produces action chunks asynchronously. The main control loop polls for the next ready action while the policy computes the next chunk in parallel.

Use RTC with large, slow VLA models (Pi0, Pi0.5, SmolVLA) for smooth, continuous motion despite high inference latency.

lerobot-rollout \
    --strategy.type=base \
    --inference.type=rtc \
    --inference.rtc.execution_horizon=10 \
    --inference.rtc.max_guidance_weight=10.0 \
    --policy.path=${HF_USER}/pi0_policy \
    --robot.type=so100_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
    --task="Pick up the cube" \
    --duration=60 \
    --device=cuda
FlagDescription
--inference.rtc.execution_horizonSteps to blend with previous chunk (default: varies by policy)
--inference.rtc.modeguided (default) or trained-prefix trained for compatible Pi05 checkpoints
--inference.rtc.max_guidance_weightConsistency enforcement strength (default: varies by policy)
--inference.rtc.prefix_attention_scheduleBlend schedule: LINEAR, EXP, ONES, ZEROS
--inference.queue_thresholdBackpressure threshold; trained RTC requires at least its maximum delay

See the Real-Time Chunking guide for details on tuning RTC parameters.


Interactive Sessions

Add --interactive=true to drive the rollout from the terminal instead of starting immediately. Hardware connects and the policy loads as usual, but the robot stays still until you type /start — useful when you want to position the scene first, re-instruct the policy between attempts, or run several takes without paying the load time again.

lerobot-rollout \
    --strategy.type=base \
    --policy.path=${HF_USER}/my_smolvla_policy \
    --robot.type=so100_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
    --task="pick up the cube" \
    --interactive=true
CommandAction
/startStart (or restart) the policy control loop
/subtask <text>Change the instruction the policy follows, without stopping. No argument prints the current task. Only affects policies that condition on language (SmolVLA, π0/π0.5, and similar)
/vqa <text>Ask the policy a question about what it currently sees. Only while a run is in progress, and only for policies with a text head
/autosteer <goal>Hand the policy a high-level goal and let it choose its own subtasks. /autosteer off hands control back; no argument reports the current goal
/resetStop movement, return the robot to its startup position, and restore the --task instruction
/stopEnd the session and run the normal shutdown routines
/helpList the commands
> /start
Rollout running — task 'pick up the cube'. /subtask <text> to change it, ...
> /subtask put the cube in the box
Task: 'pick up the cube' → 'put the cube in the box' (applies from the next policy inference)
> /vqa is the cube inside the box?
Asked: 'is the cube inside the box?' — answering from the next observation...
Q: is the cube inside the box?
A: No — the cube is on the table to the left of the box.
> /reset
Task restored to 'pick up the cube'
Resetting — returning the robot to its initial position...
Robot reset — holding at initial position. /start to run.
> /stop

Lines starting with > are what you type; the session renders no prompt of its own.

Ctrl-C still shuts down as usual, and closing stdin (Ctrl-D, or the end of a piped script) ends the session — so a piped script must keep stdin open for the intended duration:

(printf '/start\n'; sleep 60; printf '/stop\n') | lerobot-rollout ... --interactive=true

/subtask applies from the next inference. With --inference.type=sync the precomputed chunk actions are dropped, so the switch lands on the next control tick; with rtc the new chunk is blended over the previous chunk’s leftover prefix, so it lands within one inference and the motion stays continuous (with --inference.rtc.enabled=false the queued chunk drains first, delaying the switch by up to one chunk).

With --use_torch_compile=true, an instruction that tokenizes to a different length can trigger a recompilation, pausing inference for as long as the original warm-up took — leave compilation off if you expect to re-instruct often.

/vqa and /autosteer need a policy with a text head (an implementation of PreTrainedPolicy.generate_text()); no built-in LeRobot policy ships one yet, so both commands are refused unless yours does.

/vqa answers from the next observation and prints when ready, so you can keep typing meanwhile; only one question is in flight at a time (autosteer’s own queries share that slot). Text generation is far slower than a control tick, so the robot drains its queued actions and then holds position until the answer lands, and that time counts against --duration. Questions are only accepted while a run is in progress — an idle engine has no current view — and one still unanswered when a segment ends is dropped and reported.

/autosteer hands the policy a goal and asks it for the next subtask every --autosteer_interval_s seconds (default 10), applying each one through the same path as /subtask and announcing it in the chat. Every query is a full text generation, so the interval is a cost knob: lower values re-plan sooner but spend more of the loop generating text instead of acting. Plan progress lives in the policy, not the rollout, so the plan dies with the segment; the sequencer also stops on /reset, /subtask, /autosteer off, and the first planning failure.

Logs below ERROR are muted while the session runs so routine output doesn’t interleave with what you’re typing; errors and fatal inference failures still show, and normal logging resumes when the session ends. The cadence summaries are printed in the chat instead, and running without --interactive gets you the live log back.

Sessions work over SSH and on headless machines — no display server needed.

Recording while interactive. --strategy.type=sentry also accepts --interactive=true and records continuously while you steer: each /start/reset segment saves complete episodes plus one final partial episode, and the dataset stays open until shutdown. Frames carry the task provenance of the action actually sent, so actions already queued or interpolated under the old task keep the old label.

lerobot-rollout \
    --strategy.type=sentry \
    --policy.path=${HF_USER}/my_smolvla_policy \
    --robot.type=so100_follower \
    --robot.port=/dev/ttyACM0 \
    --dataset.repo_id=${HF_USER}/rollout_cube_sessions \
    --task="pick up the cube" \
    --interactive=true

The other recording strategies (episodic, DAgger, highlight) are not supported: they bind their own keyboard controls and finalize the dataset when run() exits, so they cannot be restarted segment by segment.

Programmatic control

RolloutController exposes the same operations as a library API — thread-safe start() / reset() / stop() / set_task() plus a RolloutEvent callback, with no stdin, printing or log muting attached:

from threading import Event, Thread

from lerobot.rollout import (
    LinkedEvent,
    RolloutController,
    RolloutEvent,
    build_rollout_context,
    create_strategy,
)

parent = Event()  # your application's shutdown signal
ctx = build_rollout_context(cfg, LinkedEvent(parent))  # loads policy, connects robot
strategy = create_strategy(cfg.strategy)
strategy.setup(ctx)

controller = RolloutController(strategy, ctx, on_event=print)  # or your own observer
serve_thread = Thread(target=controller.serve)  # serve() blocks; run it where you like
serve_thread.start()

controller.start()                        # robot starts executing the policy
controller.set_task("grab the red cube")  # re-instruct mid-run
controller.reset()                        # stop movement, return home, stay warm
controller.stop()                         # end serve()

serve_thread.join()
strategy.teardown(ctx)  # teardown stays with the caller

RolloutController requires the context’s shutdown event to be a LinkedEvent, built as above: the controller ends run segments through its local flag, while your parent event still forces a full shutdown. Set play_sounds=False unless you want the vocal announcements.


Common Flags

FlagDescriptionDefault
--policy.pathRequired. HF Hub model ID or local checkpoint path
--robot.typeRequired. Robot type (e.g. so100_follower, koch_follower)
--robot.portSerial port for the robot
--robot.camerasCamera configuration (JSON dict)
--fpsPolicy inference and dataset recording frequency30
--durationRun time in seconds (0 = infinite; per segment if interactive)0
--deviceTorch device (cpu, cuda, mps)auto
--taskTask description (used when no dataset is provided)
--display_dataStream telemetry to Rerun visualizationfalse
--display_ip / --display_portRemote Rerun server address
--interpolation_multiplierRobot commands per policy action (control at fps × N Hz)1
--interactiveChat session on stdin; robot idles until /start (base, sentry)false
--use_torch_compileEnable torch.compile for inferencefalse
--resumeResume a previous recording sessionfalse
--play_soundsVocal synthesis for eventstrue

Cadence Reporting

Every strategy reports what its control loop actually achieved, as do lerobot-record, lerobot-teleoperate and lerobot-replay — they all pace through the same timer. Each saved episode gets a one-line digest, and when the loop ends — including on Ctrl-C — a block covering the whole run:

Cadence (episode 1): 29.84 Hz policy vs 30 Hz target · 1200 ticks, 20.1 s measured · 6/598 cycles over the 33.3 ms budget (work mean 18.7 ms, worst 48.1 ms)
Cadence (episode 2): 29.84 Hz policy vs 30 Hz target · 1200 ticks, 20.1 s measured · 6/599 cycles over the 33.3 ms budget (work mean 18.7 ms, worst 48.1 ms)
Cadence summary — whole run, 2 episodes · target 30 Hz × 2 (16.7 ms tick slot, 33.3 ms cycle budget): 2400 ticks, 1197 cycles judged
  effective cadence: 29.84 Hz policy / 59.67 Hz commands over 40.2 s measured
  cycles over the 33.3 ms work budget: 12/1197 (1.0%) — work mean 18.7 ms, worst 48.1 ms
  ticks over their 16.7 ms slot: 40/2400 (costs interpolation smoothness only)
  loop-body steps (share of measured work):
    observe      mean   3.13 ms · worst   9.00 ms ·  31.7% of work · 2400 calls
    process_obs  mean   0.50 ms · worst   1.00 ms ·   5.1% of work · 2400 calls
    infer        mean   5.18 ms · worst  40.00 ms ·  52.4% of work · 2400 calls
    send         mean   0.40 ms · worst   0.40 ms ·   4.1% of work · 2400 calls
    record       mean   0.67 ms · worst 620.00 ms ·   6.8% of work · 2402 calls
  pacing headroom: 7.4 ms slept per tick on average (max 13.0 ms) — near zero means the loop is saturated

Read it top-down: effective cadence is what you got against --fps, cycles over budget is how often the loop could not hold it, the step breakdown says where the time went, and pacing headroom near zero means there is nothing left to absorb a slow tick. A high infer share points at the policy or the device; a high observe share points at the cameras. A step’s worst far above its mean is a one-off — above, record is 0.67 ms per frame and the 620 ms outlier is the blocking save_episode that closed an episode.

A cycle here is one policy action’s worth of ticks (--interpolation_multiplier of them). Only whole cycles over the 1/fps budget can cost you frames; a single tick overrunning its slot only makes the interpolated motion less smooth, which is why the two are counted separately. Without interpolation a cycle is a single tick, so the report drops the distinction and counts ticks throughout.

In interactive sessions each /start segment owns its own timer, so the report lands in the chat once per segment (/reset, /stop, or --duration) rather than once per process.


Programmatic Usage

For custom deployments (e.g. with kinematics processors), use the rollout module API directly:

from lerobot.rollout import BaseStrategyConfig, RolloutConfig, build_rollout_context
from lerobot.rollout.inference import SyncInferenceConfig
from lerobot.rollout.strategies import BaseStrategy
from lerobot.utils.process import ProcessSignalHandler

cfg = RolloutConfig(
    robot=my_robot_config,
    policy=my_policy_config,
    strategy=BaseStrategyConfig(),
    inference=SyncInferenceConfig(),
    fps=30,
    duration=60,
    task="my task",
)

signal_handler = ProcessSignalHandler(use_threads=True)
ctx = build_rollout_context(
    cfg,
    signal_handler.shutdown_event,
    robot_action_processor=my_custom_action_processor,       # optional
    robot_observation_processor=my_custom_obs_processor,     # optional
)

strategy = BaseStrategy(cfg.strategy)
try:
    strategy.setup(ctx)
    strategy.run(ctx)
finally:
    strategy.teardown(ctx)

See examples/so100_to_so100_EE/rollout.py and examples/phone_to_so100/rollout.py for full examples with kinematics processors.

Update on GitHub