We release Evo-RL, the opensource real-world offline RL on So-101 and AgileX PiPER for easier reproduction.
Python
817
73 commits
updated Sep 15, 2026
SJTU & Evo-Tech
Architecture Overview
Value Visual Results
Success Case
Failure Case
Policy Rollout Visual Results
Human-in-the-Loop Visual Results
LeRobot-aligned foundation: we use LeRobot as the base of this codebase because its inference and data-collection logic are highly aligned with real-world RL workflows.
git clone https://github.com/MINT-SJTU/Evo-RL.git
cd Evo-RL
conda create -y -n evo-rl python=3.10
conda activate evo-rl
pip install -e .
For setup details and platform-specific dependencies, follow the official LeRobot configuration guide.
For SO-series setup, please follow the official tutorial in detail and complete all installation and configuration steps there before continuing. The examples below use SO101 as the reference configuration.
Recommended path strategy:
/dev/serial/by-id/ (stable across reboots)./dev/v4l/by-id/; if IDs are not unique, use /dev/v4l/by-path/.by-id, camera paths use by-path.You can inspect available stable paths with:
ls -l /dev/serial/by-id/
ls -l /dev/v4l/by-id/
ls -l /dev/v4l/by-path/
For single-arm users, no major changes are required. After setup, run the command below to verify your system is ready for the next stage:
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port=/dev/serial/by-id/<SO101_FOLLOWER_PORT> \
--robot.id=my_so101_follower \
--teleop.type=so101_leader \
--teleop.port=/dev/serial/by-id/<SO101_LEADER_PORT> \
--teleop.id=my_so101_leader
For dual-arm users, we recommend mirroring the mechanical parts corresponding to servos 4/5/6 on the left leader and left follower arms, which usually provides a more natural bimanual operation feel.
Before running the dual-arm command, make sure calibration files exist under ~/.cache/huggingface/lerobot/calibration/ like:
calibration/
โโโ robots
โ โโโ so_follower
โ โโโ bi_so101_follower_left.json
โ โโโ bi_so101_follower_right.json
โโโ teleoperators
โโโ so_leader
โโโ bi_so101_leader_left.json
โโโ bi_so101_leader_right.json
This layout is slightly different from single-arm setup.
Then run this command to verify dual-arm setup:
lerobot-teleoperate \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/serial/by-id/<LEFT_FOLLOWER_PORT> \
--robot.right_arm_config.port=/dev/serial/by-id/<RIGHT_FOLLOWER_PORT> \
--robot.id=bi_so101_follower \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/serial/by-id/<LEFT_LEADER_PORT> \
--teleop.right_arm_config.port=/dev/serial/by-id/<RIGHT_LEADER_PORT> \
--teleop.id=bi_so101_leader
Before data collection, validate camera mapping first.
Check whether each camera supports your target setting (for example, 640x480 @ 30):
v4l2-ctl -d /dev/v4l/by-path/<CAM_PATH> --list-formats-ext
Single-arm camera check (example):
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port=/dev/serial/by-id/<SO101_FOLLOWER_PORT> \
--robot.id=my_so101_follower \
--robot.cameras='{ front: {type: opencv, index_or_path: "/dev/v4l/by-path/<FRONT_CAM>", width: 640, height: 480, fps: 30}}' \
--teleop.type=so101_leader \
--teleop.port=/dev/serial/by-id/<SO101_LEADER_PORT> \
--teleop.id=my_so101_leader \
--display_data=true
Dual-arm camera check (example):
lerobot-teleoperate \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/serial/by-id/<LEFT_FOLLOWER_PORT> \
--robot.right_arm_config.port=/dev/serial/by-id/<RIGHT_FOLLOWER_PORT> \
--robot.id=my_bi_so101_follower \
--robot.left_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<LEFT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30}}' \
--robot.right_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<RIGHT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30}, front: {type: opencv, index_or_path: "/dev/v4l/by-path/<FRONT_CAM_PATH>", width: 640, height: 480, fps: 30}}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/serial/by-id/<LEFT_LEADER_PORT> \
--teleop.right_arm_config.port=/dev/serial/by-id/<RIGHT_LEADER_PORT> \
--teleop.id=my_bi_so101_leader \
--display_data=true
For dual-arm camera mapping, it is fine to attach front under either the left-arm or right-arm camera config. If you use more camera views, place them under either the left or right arm camera config as well.
If needed, you can also use temporary device paths (for example /dev/ttyACM* and /dev/video*) during initial debugging.
PiPER commands use the USB-CAN adapter's stable ID_SERIAL_SHORT instead of a temporary Linux
canN name. List the connected adapters with:
for device in /sys/class/net/*; do
[ "$(cat "$device/type" 2>/dev/null)" = "280" ] || continue
serial=$(udevadm info --query=property --path="$device" | sed -n 's/^ID_SERIAL_SHORT=//p')
echo "$(basename "$device"): $serial"
done
Then configure the adapters needed for the run:
lerobot-setup-can --mode=setup \
--usb_can_serials=<USB_CAN_SERIAL_1>,<USB_CAN_SERIAL_2>,<USB_CAN_SERIAL_3>,<USB_CAN_SERIAL_4>
For single-arm users, run the command below to verify the system is ready:
lerobot-teleoperate \
--robot.type=piperx_follower \
--robot.port=<FOLLOWER_USB_CAN_SERIAL> \
--robot.id=my_piperx_follower \
--teleop.type=piperx_leader \
--teleop.port=<LEADER_USB_CAN_SERIAL> \
--teleop.id=my_piperx_leader
For bimanual users, run this command to verify dual-arm teleoperation:
lerobot-teleoperate \
--robot.type=bi_piperx_follower \
--robot.id=my_bi_piperx_follower \
--robot.left_arm_config.port=<LEFT_FOLLOWER_USB_CAN_SERIAL> \
--robot.right_arm_config.port=<RIGHT_FOLLOWER_USB_CAN_SERIAL> \
--teleop.type=bi_piperx_leader \
--teleop.id=my_bi_piperx_leader \
--teleop.left_arm_config.port=<LEFT_LEADER_USB_CAN_SERIAL> \
--teleop.right_arm_config.port=<RIGHT_LEADER_USB_CAN_SERIAL>
For PiPER (non-X), replace bi_piperx_follower/bi_piperx_leader with bi_piper_follower/bi_piper_leader.
Collect rollout data with lerobot-human-inloop-record.
Bimanual template:
lerobot-human-inloop-record \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/serial/by-id/<LEFT_FOLLOWER_PORT> \
--robot.right_arm_config.port=/dev/serial/by-id/<RIGHT_FOLLOWER_PORT> \
--robot.id=my_bi_so101_follower \
--robot.left_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<LEFT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30, fourcc: "MJPG"}}' \
--robot.right_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<RIGHT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30, fourcc: "MJPG"}, front: {type: intelrealsense, serial_number_or_name: "<REALSENSE_SN>", width: 640, height: 480, fps: 30, warmup_s: 2}}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/serial/by-id/<LEFT_LEADER_PORT> \
--teleop.right_arm_config.port=/dev/serial/by-id/<RIGHT_LEADER_PORT> \
--teleop.id=my_bi_so101_leader \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME> \
--dataset.single_task="<YOUR_TASK_DESCRIPTION>" \
--dataset.num_episodes=<NUM_EPISODES> \
--dataset.episode_time_s=<EPISODE_SECONDS> \
--dataset.reset_time_s=<RESET_SECONDS> \
--dataset.push_to_hub=true \
--display_data=true
Recommendation: use fourcc: "MJPG" for OpenCV and warmup_s for RealSense. In this example front uses RealSense, but you can switch it to OpenCV with the same structure.
Bimanual template (left/right, PiPER-X example):
lerobot-human-inloop-record \
--robot.type=bi_piperx_follower \
--robot.id=my_bi_piperx_follower \
--robot.left_arm_config.port=<LEFT_FOLLOWER_USB_CAN_SERIAL> \
--robot.right_arm_config.port=<RIGHT_FOLLOWER_USB_CAN_SERIAL> \
--teleop.type=bi_piperx_leader \
--teleop.id=my_bi_piperx_leader \
--teleop.left_arm_config.port=<LEFT_LEADER_USB_CAN_SERIAL> \
--teleop.right_arm_config.port=<RIGHT_LEADER_USB_CAN_SERIAL> \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME> \
--dataset.single_task="<YOUR_TASK_DESCRIPTION>" \
--dataset.num_episodes=<NUM_EPISODES> \
--dataset.episode_time_s=<EPISODE_SECONDS> \
--dataset.reset_time_s=<RESET_SECONDS> \
--dataset.push_to_hub=true \
--display_data=true
Hotkeys:
i: toggle intervention mode (policy <-> teleop takeover)s: mark success and end current episodef: mark failure and end current episodeRight Arrow: end the current loop earlyLeft Arrow: end early and re-record the current episodeEsc: stop the recording sessionQuick quality check:
lerobot-dataset-report --dataset <HF_USERNAME_OR_ORG>/<DATASET_NAME>
This prints: dataset meta, totals, episode-length stats/histogram, success/intervention metrics, task list, and full feature schema.
Train the value function on the current dataset. Current default: Pi*0.6 (--value.type=pistar06).
Single-GPU template:
lerobot-value-train \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME> \
--value.type=pistar06 \
--value.dtype=bfloat16 \
--value.push_to_hub=true \
--value.repo_id=<HF_USERNAME_OR_ORG>/<VALUE_MODEL_REPO> \
--batch_size=64 \
--output_dir=outputs/value_train/<RUN_NAME> \
--job_name=<RUN_NAME> \
--wandb.enable=true
Multi-GPU template:
CUDA_VISIBLE_DEVICES=<GPU_ID_LIST> accelerate launch \
--multi_gpu \
--num_processes=<NUM_GPUS> \
--mixed_precision=bf16 \
$(which lerobot-value-train) \
--batch_size=32/<NUM_GPUS> \
<VALUE_TRAIN_ARGS>
To plug in a different value function, minimal path in this repo:
src/lerobot/values/<your_value>/configuration_<your_value>.py with @PreTrainedConfig.register_subclass("<your_value>").src/lerobot/values/<your_value>/modeling_<your_value>.py with <YourValue>Policy(PreTrainedPolicy) (implement at least forward, predict_value, and build_training_raw_batch_hook for lerobot-value-train).src/lerobot/values/<your_value>/processor_<your_value>.py with make_<your_value>_pre_post_processors(...).pistar06-only type checks in src/lerobot/configs/value_train.py and src/lerobot/scripts/lerobot_value_infer.py.Infer value signals and write value/advantage/indicator back to the dataset:
value: estimated return-to-go of the current frame.advantage: relative improvement signal (higher means better-than-baseline trajectory quality).indicator: binarized training tag derived from advantage.Single-GPU template:
lerobot-value-infer \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME> \
--inference.checkpoint_path=outputs/value_train/<RUN_NAME> \
--runtime.device=cuda \
--runtime.batch_size=64 \
--acp.enable=true \
--acp.n_step=50 \
--acp.positive_ratio=0.3 \
--acp.value_field=complementary_info.value_<TAG> \
--acp.advantage_field=complementary_info.advantage_<TAG> \
--acp.indicator_field=complementary_info.acp_indicator_<TAG> \
--output_dir=outputs/value_infer/<RUN_NAME> \
--job_name=<RUN_NAME>.infer
Multi-GPU template:
CUDA_VISIBLE_DEVICES=<GPU_ID_LIST> accelerate launch \
--multi_gpu \
--num_processes=<NUM_GPUS> \
--mixed_precision=bf16 \
$(which lerobot-value-infer) \
<VALUE_INFER_ARGS>
Parameter notes:
--acp.n_step: n-step advantage horizon.
--acp.positive_ratio: positive label ratio after advantage binarization (e.g., 0.3 = top 30% per task).
Expected new columns:
complementary_info.value_<TAG>
complementary_info.advantage_<TAG>
complementary_info.acp_indicator_<TAG>
These columns are written back to the original dataset specified by --dataset.repo_id.
Train the policy with advantage-conditioned tags. Policy requirement: it must support text/task input, because Advantage-Conditioned tags are injected into task text.
Single-GPU template:
lerobot-train \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME> \
--policy.type=<POLICY_TYPE> \
--policy.pretrained_path=<POLICY_PRETRAINED_PATH> \
--policy.device=cuda \
--policy.dtype=bfloat16 \
--batch_size=32 \
--steps=30000 \
--acp.enable=true \
--acp.indicator_field=complementary_info.acp_indicator_<TAG> \
--acp.indicator_dropout_prob=0.3 \
--output_dir=outputs/train/<RUN_NAME> \
--job_name=<RUN_NAME> \
--wandb.enable=true \
--policy.push_to_hub=true \
--policy.repo_id=<HF_USERNAME_OR_ORG>/<POLICY_REPO>
--acp.indicator_dropout_prob controls tag drop rate in task text; 0.3 helps learn both tagged and untagged conditions.
Important checks:
--acp.indicator_field must exist in the dataset and be binary (0/1).Multi-GPU template:
CUDA_VISIBLE_DEVICES=<GPU_ID_LIST> accelerate launch \
--multi_gpu \
--num_processes=<NUM_GPUS> \
--mixed_precision=bf16 \
$(which lerobot-train) \
--batch_size=32/<NUM_GPUS> \
<POLICY_TRAIN_ARGS>
Deploy the trained policy in human-in-loop mode and collect the next dataset round:
lerobot-human-inloop-record \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/serial/by-id/<LEFT_FOLLOWER_PORT> \
--robot.right_arm_config.port=/dev/serial/by-id/<RIGHT_FOLLOWER_PORT> \
--robot.id=my_bi_so101_follower \
--robot.left_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<LEFT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30, fourcc: "MJPG"}}' \
--robot.right_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<RIGHT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30, fourcc: "MJPG"}, front: {type: intelrealsense, serial_number_or_name: "<REALSENSE_SN>", width: 640, height: 480, fps: 30, warmup_s: 2}}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/serial/by-id/<LEFT_LEADER_PORT> \
--teleop.right_arm_config.port=/dev/serial/by-id/<RIGHT_LEADER_PORT> \
--teleop.id=my_bi_so101_leader \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME_NEXT_ROUND> \
--dataset.single_task="<YOUR_TASK_DESCRIPTION>" \
--dataset.num_episodes=<NUM_EPISODES> \
--dataset.episode_time_s=<EPISODE_SECONDS> \
--dataset.reset_time_s=<RESET_SECONDS> \
--dataset.push_to_hub=true \
--display_data=true \
--policy.path=<POLICY_CHECKPOINT_OR_HUB_ID> \
--resume=true
Dataset continuation options:
--resume=true and continue recording into the same dataset.lerobot-edit-dataset \
--repo_id=<HF_USERNAME_OR_ORG>/<MERGED_DATASET_NAME> \
--operation.type=merge \
--operation.repo_ids="['<HF_USERNAME_OR_ORG>/<DATASET_ROUND_1>','<HF_USERNAME_OR_ORG>/<DATASET_ROUND_2>']"
Additional data attributes vs default lerobot-record behavior:
complementary_info.policy_action: policy output action at each step.complementary_info.is_intervention: whether current step is in intervention.complementary_info.state: intervention state-machine state.complementary_info.collector_policy_id: step-level action source ID (human or policy ID).episode_success: success/failure label saved per episode.Iterative training loop (abstract):
[Multi-task demonstration data pool]
|
v
[Offline RL pretraining for a vision-language-action policy]
|
v
[Task-specific initialization / fine-tuning from demonstrations]
|
v
|---- Iteration k = 1..K -------------------------------------|
| 1) Deploy current policy ฯ_k and collect new rollout data |
| 2) Merge into data pool: D <- D U new_data |
| 3) Train value function on D |
| 4) Infer advantage and binarize into indicator tags |
| 5) Train advantage-conditioned policy to get ฯ_{k+1} |
|-------------------------------------------------------------|
|
v
[Stronger policy with improved success rate and throughput]
RW-RL Dataset is the companion real-world reinforcement learning dataset for Evo-RL. It is organized around iterative policy improvement on real robots, including teleoperation demonstrations, human-in-the-loop intervention data, policy rollout traces, episode-level success/failure labels, intervention states, and complementary signals used for value/reward modeling.
The dataset is designed to support offline RL, value learning, advantage-conditioned policy training, and closed-loop rollout analysis across real robot tasks. Please refer to the Hugging Face dataset card for release notes, schema details, splits, and version tags.
docs/README.md
@misc{evorl2026,
title = {Evo-RL: Towards Iterative Policy Improvement in Real-World Offline RL},
author = {Evo-RL Contributors},
year = {2026},
howpublished = {\url{https://github.com/MINT-SJTU/Evo-RL}}
}
Apache-2.0. See LICENSE.
Python
98.8%
We release Evo-RL, the opensource real-world offline RL on So-101 and AgileX PiPER for easier reproduction.
Python
817
73 commits
updated Sep 15, 2026
SJTU & Evo-Tech
Architecture Overview
Value Visual Results
Success Case
Failure Case
Policy Rollout Visual Results
Human-in-the-Loop Visual Results
LeRobot-aligned foundation: we use LeRobot as the base of this codebase because its inference and data-collection logic are highly aligned with real-world RL workflows.
git clone https://github.com/MINT-SJTU/Evo-RL.git
cd Evo-RL
conda create -y -n evo-rl python=3.10
conda activate evo-rl
pip install -e .
For setup details and platform-specific dependencies, follow the official LeRobot configuration guide.
For SO-series setup, please follow the official tutorial in detail and complete all installation and configuration steps there before continuing. The examples below use SO101 as the reference configuration.
Recommended path strategy:
/dev/serial/by-id/ (stable across reboots)./dev/v4l/by-id/; if IDs are not unique, use /dev/v4l/by-path/.by-id, camera paths use by-path.You can inspect available stable paths with:
ls -l /dev/serial/by-id/
ls -l /dev/v4l/by-id/
ls -l /dev/v4l/by-path/
For single-arm users, no major changes are required. After setup, run the command below to verify your system is ready for the next stage:
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port=/dev/serial/by-id/<SO101_FOLLOWER_PORT> \
--robot.id=my_so101_follower \
--teleop.type=so101_leader \
--teleop.port=/dev/serial/by-id/<SO101_LEADER_PORT> \
--teleop.id=my_so101_leader
For dual-arm users, we recommend mirroring the mechanical parts corresponding to servos 4/5/6 on the left leader and left follower arms, which usually provides a more natural bimanual operation feel.
Before running the dual-arm command, make sure calibration files exist under ~/.cache/huggingface/lerobot/calibration/ like:
calibration/
โโโ robots
โ โโโ so_follower
โ โโโ bi_so101_follower_left.json
โ โโโ bi_so101_follower_right.json
โโโ teleoperators
โโโ so_leader
โโโ bi_so101_leader_left.json
โโโ bi_so101_leader_right.json
This layout is slightly different from single-arm setup.
Then run this command to verify dual-arm setup:
lerobot-teleoperate \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/serial/by-id/<LEFT_FOLLOWER_PORT> \
--robot.right_arm_config.port=/dev/serial/by-id/<RIGHT_FOLLOWER_PORT> \
--robot.id=bi_so101_follower \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/serial/by-id/<LEFT_LEADER_PORT> \
--teleop.right_arm_config.port=/dev/serial/by-id/<RIGHT_LEADER_PORT> \
--teleop.id=bi_so101_leader
Before data collection, validate camera mapping first.
Check whether each camera supports your target setting (for example, 640x480 @ 30):
v4l2-ctl -d /dev/v4l/by-path/<CAM_PATH> --list-formats-ext
Single-arm camera check (example):
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port=/dev/serial/by-id/<SO101_FOLLOWER_PORT> \
--robot.id=my_so101_follower \
--robot.cameras='{ front: {type: opencv, index_or_path: "/dev/v4l/by-path/<FRONT_CAM>", width: 640, height: 480, fps: 30}}' \
--teleop.type=so101_leader \
--teleop.port=/dev/serial/by-id/<SO101_LEADER_PORT> \
--teleop.id=my_so101_leader \
--display_data=true
Dual-arm camera check (example):
lerobot-teleoperate \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/serial/by-id/<LEFT_FOLLOWER_PORT> \
--robot.right_arm_config.port=/dev/serial/by-id/<RIGHT_FOLLOWER_PORT> \
--robot.id=my_bi_so101_follower \
--robot.left_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<LEFT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30}}' \
--robot.right_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<RIGHT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30}, front: {type: opencv, index_or_path: "/dev/v4l/by-path/<FRONT_CAM_PATH>", width: 640, height: 480, fps: 30}}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/serial/by-id/<LEFT_LEADER_PORT> \
--teleop.right_arm_config.port=/dev/serial/by-id/<RIGHT_LEADER_PORT> \
--teleop.id=my_bi_so101_leader \
--display_data=true
For dual-arm camera mapping, it is fine to attach front under either the left-arm or right-arm camera config. If you use more camera views, place them under either the left or right arm camera config as well.
If needed, you can also use temporary device paths (for example /dev/ttyACM* and /dev/video*) during initial debugging.
PiPER commands use the USB-CAN adapter's stable ID_SERIAL_SHORT instead of a temporary Linux
canN name. List the connected adapters with:
for device in /sys/class/net/*; do
[ "$(cat "$device/type" 2>/dev/null)" = "280" ] || continue
serial=$(udevadm info --query=property --path="$device" | sed -n 's/^ID_SERIAL_SHORT=//p')
echo "$(basename "$device"): $serial"
done
Then configure the adapters needed for the run:
lerobot-setup-can --mode=setup \
--usb_can_serials=<USB_CAN_SERIAL_1>,<USB_CAN_SERIAL_2>,<USB_CAN_SERIAL_3>,<USB_CAN_SERIAL_4>
For single-arm users, run the command below to verify the system is ready:
lerobot-teleoperate \
--robot.type=piperx_follower \
--robot.port=<FOLLOWER_USB_CAN_SERIAL> \
--robot.id=my_piperx_follower \
--teleop.type=piperx_leader \
--teleop.port=<LEADER_USB_CAN_SERIAL> \
--teleop.id=my_piperx_leader
For bimanual users, run this command to verify dual-arm teleoperation:
lerobot-teleoperate \
--robot.type=bi_piperx_follower \
--robot.id=my_bi_piperx_follower \
--robot.left_arm_config.port=<LEFT_FOLLOWER_USB_CAN_SERIAL> \
--robot.right_arm_config.port=<RIGHT_FOLLOWER_USB_CAN_SERIAL> \
--teleop.type=bi_piperx_leader \
--teleop.id=my_bi_piperx_leader \
--teleop.left_arm_config.port=<LEFT_LEADER_USB_CAN_SERIAL> \
--teleop.right_arm_config.port=<RIGHT_LEADER_USB_CAN_SERIAL>
For PiPER (non-X), replace bi_piperx_follower/bi_piperx_leader with bi_piper_follower/bi_piper_leader.
Collect rollout data with lerobot-human-inloop-record.
Bimanual template:
lerobot-human-inloop-record \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/serial/by-id/<LEFT_FOLLOWER_PORT> \
--robot.right_arm_config.port=/dev/serial/by-id/<RIGHT_FOLLOWER_PORT> \
--robot.id=my_bi_so101_follower \
--robot.left_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<LEFT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30, fourcc: "MJPG"}}' \
--robot.right_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<RIGHT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30, fourcc: "MJPG"}, front: {type: intelrealsense, serial_number_or_name: "<REALSENSE_SN>", width: 640, height: 480, fps: 30, warmup_s: 2}}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/serial/by-id/<LEFT_LEADER_PORT> \
--teleop.right_arm_config.port=/dev/serial/by-id/<RIGHT_LEADER_PORT> \
--teleop.id=my_bi_so101_leader \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME> \
--dataset.single_task="<YOUR_TASK_DESCRIPTION>" \
--dataset.num_episodes=<NUM_EPISODES> \
--dataset.episode_time_s=<EPISODE_SECONDS> \
--dataset.reset_time_s=<RESET_SECONDS> \
--dataset.push_to_hub=true \
--display_data=true
Recommendation: use fourcc: "MJPG" for OpenCV and warmup_s for RealSense. In this example front uses RealSense, but you can switch it to OpenCV with the same structure.
Bimanual template (left/right, PiPER-X example):
lerobot-human-inloop-record \
--robot.type=bi_piperx_follower \
--robot.id=my_bi_piperx_follower \
--robot.left_arm_config.port=<LEFT_FOLLOWER_USB_CAN_SERIAL> \
--robot.right_arm_config.port=<RIGHT_FOLLOWER_USB_CAN_SERIAL> \
--teleop.type=bi_piperx_leader \
--teleop.id=my_bi_piperx_leader \
--teleop.left_arm_config.port=<LEFT_LEADER_USB_CAN_SERIAL> \
--teleop.right_arm_config.port=<RIGHT_LEADER_USB_CAN_SERIAL> \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME> \
--dataset.single_task="<YOUR_TASK_DESCRIPTION>" \
--dataset.num_episodes=<NUM_EPISODES> \
--dataset.episode_time_s=<EPISODE_SECONDS> \
--dataset.reset_time_s=<RESET_SECONDS> \
--dataset.push_to_hub=true \
--display_data=true
Hotkeys:
i: toggle intervention mode (policy <-> teleop takeover)s: mark success and end current episodef: mark failure and end current episodeRight Arrow: end the current loop earlyLeft Arrow: end early and re-record the current episodeEsc: stop the recording sessionQuick quality check:
lerobot-dataset-report --dataset <HF_USERNAME_OR_ORG>/<DATASET_NAME>
This prints: dataset meta, totals, episode-length stats/histogram, success/intervention metrics, task list, and full feature schema.
Train the value function on the current dataset. Current default: Pi*0.6 (--value.type=pistar06).
Single-GPU template:
lerobot-value-train \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME> \
--value.type=pistar06 \
--value.dtype=bfloat16 \
--value.push_to_hub=true \
--value.repo_id=<HF_USERNAME_OR_ORG>/<VALUE_MODEL_REPO> \
--batch_size=64 \
--output_dir=outputs/value_train/<RUN_NAME> \
--job_name=<RUN_NAME> \
--wandb.enable=true
Multi-GPU template:
CUDA_VISIBLE_DEVICES=<GPU_ID_LIST> accelerate launch \
--multi_gpu \
--num_processes=<NUM_GPUS> \
--mixed_precision=bf16 \
$(which lerobot-value-train) \
--batch_size=32/<NUM_GPUS> \
<VALUE_TRAIN_ARGS>
To plug in a different value function, minimal path in this repo:
src/lerobot/values/<your_value>/configuration_<your_value>.py with @PreTrainedConfig.register_subclass("<your_value>").src/lerobot/values/<your_value>/modeling_<your_value>.py with <YourValue>Policy(PreTrainedPolicy) (implement at least forward, predict_value, and build_training_raw_batch_hook for lerobot-value-train).src/lerobot/values/<your_value>/processor_<your_value>.py with make_<your_value>_pre_post_processors(...).pistar06-only type checks in src/lerobot/configs/value_train.py and src/lerobot/scripts/lerobot_value_infer.py.Infer value signals and write value/advantage/indicator back to the dataset:
value: estimated return-to-go of the current frame.advantage: relative improvement signal (higher means better-than-baseline trajectory quality).indicator: binarized training tag derived from advantage.Single-GPU template:
lerobot-value-infer \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME> \
--inference.checkpoint_path=outputs/value_train/<RUN_NAME> \
--runtime.device=cuda \
--runtime.batch_size=64 \
--acp.enable=true \
--acp.n_step=50 \
--acp.positive_ratio=0.3 \
--acp.value_field=complementary_info.value_<TAG> \
--acp.advantage_field=complementary_info.advantage_<TAG> \
--acp.indicator_field=complementary_info.acp_indicator_<TAG> \
--output_dir=outputs/value_infer/<RUN_NAME> \
--job_name=<RUN_NAME>.infer
Multi-GPU template:
CUDA_VISIBLE_DEVICES=<GPU_ID_LIST> accelerate launch \
--multi_gpu \
--num_processes=<NUM_GPUS> \
--mixed_precision=bf16 \
$(which lerobot-value-infer) \
<VALUE_INFER_ARGS>
Parameter notes:
--acp.n_step: n-step advantage horizon.
--acp.positive_ratio: positive label ratio after advantage binarization (e.g., 0.3 = top 30% per task).
Expected new columns:
complementary_info.value_<TAG>
complementary_info.advantage_<TAG>
complementary_info.acp_indicator_<TAG>
These columns are written back to the original dataset specified by --dataset.repo_id.
Train the policy with advantage-conditioned tags. Policy requirement: it must support text/task input, because Advantage-Conditioned tags are injected into task text.
Single-GPU template:
lerobot-train \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME> \
--policy.type=<POLICY_TYPE> \
--policy.pretrained_path=<POLICY_PRETRAINED_PATH> \
--policy.device=cuda \
--policy.dtype=bfloat16 \
--batch_size=32 \
--steps=30000 \
--acp.enable=true \
--acp.indicator_field=complementary_info.acp_indicator_<TAG> \
--acp.indicator_dropout_prob=0.3 \
--output_dir=outputs/train/<RUN_NAME> \
--job_name=<RUN_NAME> \
--wandb.enable=true \
--policy.push_to_hub=true \
--policy.repo_id=<HF_USERNAME_OR_ORG>/<POLICY_REPO>
--acp.indicator_dropout_prob controls tag drop rate in task text; 0.3 helps learn both tagged and untagged conditions.
Important checks:
--acp.indicator_field must exist in the dataset and be binary (0/1).Multi-GPU template:
CUDA_VISIBLE_DEVICES=<GPU_ID_LIST> accelerate launch \
--multi_gpu \
--num_processes=<NUM_GPUS> \
--mixed_precision=bf16 \
$(which lerobot-train) \
--batch_size=32/<NUM_GPUS> \
<POLICY_TRAIN_ARGS>
Deploy the trained policy in human-in-loop mode and collect the next dataset round:
lerobot-human-inloop-record \
--robot.type=bi_so_follower \
--robot.left_arm_config.port=/dev/serial/by-id/<LEFT_FOLLOWER_PORT> \
--robot.right_arm_config.port=/dev/serial/by-id/<RIGHT_FOLLOWER_PORT> \
--robot.id=my_bi_so101_follower \
--robot.left_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<LEFT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30, fourcc: "MJPG"}}' \
--robot.right_arm_config.cameras='{ wrist: {type: opencv, index_or_path: "/dev/v4l/by-path/<RIGHT_WRIST_CAM_PATH>", width: 640, height: 480, fps: 30, fourcc: "MJPG"}, front: {type: intelrealsense, serial_number_or_name: "<REALSENSE_SN>", width: 640, height: 480, fps: 30, warmup_s: 2}}' \
--teleop.type=bi_so_leader \
--teleop.left_arm_config.port=/dev/serial/by-id/<LEFT_LEADER_PORT> \
--teleop.right_arm_config.port=/dev/serial/by-id/<RIGHT_LEADER_PORT> \
--teleop.id=my_bi_so101_leader \
--dataset.repo_id=<HF_USERNAME_OR_ORG>/<DATASET_NAME_NEXT_ROUND> \
--dataset.single_task="<YOUR_TASK_DESCRIPTION>" \
--dataset.num_episodes=<NUM_EPISODES> \
--dataset.episode_time_s=<EPISODE_SECONDS> \
--dataset.reset_time_s=<RESET_SECONDS> \
--dataset.push_to_hub=true \
--display_data=true \
--policy.path=<POLICY_CHECKPOINT_OR_HUB_ID> \
--resume=true
Dataset continuation options:
--resume=true and continue recording into the same dataset.lerobot-edit-dataset \
--repo_id=<HF_USERNAME_OR_ORG>/<MERGED_DATASET_NAME> \
--operation.type=merge \
--operation.repo_ids="['<HF_USERNAME_OR_ORG>/<DATASET_ROUND_1>','<HF_USERNAME_OR_ORG>/<DATASET_ROUND_2>']"
Additional data attributes vs default lerobot-record behavior:
complementary_info.policy_action: policy output action at each step.complementary_info.is_intervention: whether current step is in intervention.complementary_info.state: intervention state-machine state.complementary_info.collector_policy_id: step-level action source ID (human or policy ID).episode_success: success/failure label saved per episode.Iterative training loop (abstract):
[Multi-task demonstration data pool]
|
v
[Offline RL pretraining for a vision-language-action policy]
|
v
[Task-specific initialization / fine-tuning from demonstrations]
|
v
|---- Iteration k = 1..K -------------------------------------|
| 1) Deploy current policy ฯ_k and collect new rollout data |
| 2) Merge into data pool: D <- D U new_data |
| 3) Train value function on D |
| 4) Infer advantage and binarize into indicator tags |
| 5) Train advantage-conditioned policy to get ฯ_{k+1} |
|-------------------------------------------------------------|
|
v
[Stronger policy with improved success rate and throughput]
RW-RL Dataset is the companion real-world reinforcement learning dataset for Evo-RL. It is organized around iterative policy improvement on real robots, including teleoperation demonstrations, human-in-the-loop intervention data, policy rollout traces, episode-level success/failure labels, intervention states, and complementary signals used for value/reward modeling.
The dataset is designed to support offline RL, value learning, advantage-conditioned policy training, and closed-loop rollout analysis across real robot tasks. Please refer to the Hugging Face dataset card for release notes, schema details, splits, and version tags.
docs/README.md
@misc{evorl2026,
title = {Evo-RL: Towards Iterative Policy Improvement in Real-World Offline RL},
author = {Evo-RL Contributors},
year = {2026},
howpublished = {\url{https://github.com/MINT-SJTU/Evo-RL}}
}
Apache-2.0. See LICENSE.
Python
98.8%