How LeRobot is organized
5 min read · for SO-101 users
What this means for you: when you run lerobot-record, you're
using the robot driver + teleop + dataset format. When you run
lerobot-train, you're using the dataset + trainer + policy.
Every other page on this site zooms into one of those five pieces.
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Teleop │───▶│ Robot │───▶│ Dataset │
│ (leader)│ │(follower)│ │ (record) │
└─────────┘ └──────────┘ └────┬─────┘
▼
┌──────────┐ ┌──────────┐
│ Policy │◀───│ Trainer │
│ (ACT…) │ │ (train) │
└──────────┘ └──────────┘
Recording flows left-to-right; training flows right-to-left. A trained policy then drives the robot directly during evaluation.
The basics
What is a policy?
A policy is the trained brain. You feed it what the robot sees (camera frames + joint angles) and it outputs what the robot should do next (target joint positions, or a short chunk of upcoming motions). ACT, Diffusion, SmolVLA, π0 are all different recipes for that brain.
What is a teleop?
A teleop (teleoperator) is whatever you use to drive the robot while recording demos. For SO-101 that's almost always a leader arm — a smaller twin you grip with your hand. The follower mirrors your motion. Other options: keyboard, gamepad, or a phone for 6-DoF (six degrees of freedom) tracking.
What's a robot driver?
The piece that talks to the actual motors over a USB or CAN bus. It reads joint angles and writes target positions every loop iteration. You don't usually touch it directly; the CLI scripts use it for you.
What's a LeRobotDataset?
A folder of recorded demos saved as Parquet tables (the numeric data: joint angles, actions, timestamps) plus MP4 videos (the camera frames), with a small JSON index that ties them together. It uploads cleanly to the Hugging Face Hub.
What ties it together?
The CLI scripts (lerobot-record, lerobot-train,
lerobot-eval) and a set of config dataclasses
— plain Python classes whose fields become CLI flags. Almost every
flag you've ever passed is documented as one of those fields, so
--help works on everything.
How you actually use it
The three CLI commands you'll hit most:
A typical SO-101 session: record 50 episodes → push the dataset to
the Hub → fine-tune an ACT or Diffusion checkpoint on it →
load the fine-tuned policy back and run lerobot-eval. All
five Lego pieces, in order.
Things to know
LeRobot ships drivers for SO-100/101, Koch v1.0/v1.1, ALOHA-style bimanual setups, OpenManipulator-X, OpenArm, LeKiwi, Reachy 2, Hope Jr., and Unitree G1. If you're on SO-101, you're on the most well-trodden path.
Pretty much every CLI flag corresponds to a field on a config
dataclass. --help on any lerobot-* command
will print the full set of options for the type you've selected
(e.g. --policy.type=act swaps in the ACT-specific flags).
Same five Lego pieces, same five subdirectories, same CLI commands. The structural changes are: one new shared backbone file for the π-family policies, the Unitree G1 driver split into smaller modules, and a single new training-config field for deterministic cuDNN. If you're an SO-101 user, none of that touches your workflow.
Show the 18 subpackages and what each owns
18 subpackages, identical roster in v0.4.4 and v0.5.0. Vendor means code copied from an upstream model repo (HF Transformers / OpenPI / Florence-2) and patched in-tree.
| Package | Kind | Owns / responsibility | Δ 0.4.4 → 0.5.0 |
|---|---|---|---|
async_inference/ |
first-party | gRPC async-inference pipeline: policy_server.py, robot_client.py, helpers (make_lerobot_observation at src/lerobot/async_inference/helpers.py:135). |
5-line import block in robot_client.py |
cameras/ |
first-party | Camera ABC at src/lerobot/cameras/camera.py:26; backends opencv/, realsense/, reachy2_camera/, zmq/; factory make_cameras_from_configs at src/lerobot/cameras/utils.py:25. |
zmq/image_server.py adds CaptureThread |
configs/ |
first-party | Top-level dataclass configs (TrainPipelineConfig, EvalPipelineConfig, PreTrainedConfig), the draccus-based CLI parser, shared enums. |
+1 field (cudnn_deterministic) |
data_processing/ |
first-party | Single helper module sarm_annotations/subtask_annotation.py for SARM (Subtask-Annotation Reward Model) labels. |
unchanged |
datasets/ |
first-party | LeRobotDataset + MultiLeRobotDataset (src/lerobot/datasets/lerobot_dataset.py:566,1722); metadata at :86; v2.1→v3.0 migration in v30/; streaming, online buffer, stats, transforms. |
cosmetic (PEP 695, pd.Index name) |
envs/ |
first-party | Gym env config + factory: make_env at src/lerobot/envs/factory.py:102; Libero + Metaworld wrappers; EnvHub support. |
unchanged |
model/ |
first-party | Currently a single file: kinematics.py. Used by Unitree G1 and so_follower. |
unchanged |
motors/ |
first-party | Motor-bus abstractions (motors_bus.py), backends dynamixel/, feetech/, damiao/, robstride/, calibration_gui.py. |
cosmetic (TypeAlias → type) |
optim/ |
first-party | Optimizer + LR-scheduler dataclass configs and factory make_optimizer_and_scheduler at src/lerobot/optim/factory.py:25. |
byte-identical |
policies/ |
mixed | Per-policy folders (act/, diffusion/, tdmpc/, vqbet/, sac/, smolvla/, pi0/, pi05/, pi0_fast/, groot/, xvla/, wall_x/, sarm/, rtc/); base PreTrainedPolicy at src/lerobot/policies/pretrained.py:44; central factory.py. |
+ pi_gemma.py |
processor/ |
first-party | Pipeline ABC + step registry. The thing that makes training policy-agnostic: each policy folder ships a processor_<name>.py that builds a pre/post pair from these steps. |
cosmetic (PEP 695) |
rl/ |
first-party | HIL-SERL / online-RL stack: actor.py, learner.py, buffer.py, gym_manipulator.py, learner_service.py (gRPC), wandb_utils.py. |
unchanged |
robots/ |
first-party | Robot ABC at src/lerobot/robots/robot.py:30; per-platform packages (koch_follower/, so_follower/, bi_so_follower/, lekiwi/, reachy2/, unitree_g1/, hope_jr/, omx_follower/, openarm_follower/, bi_openarm_follower/, earthrover_mini_plus/); factory make_robot_from_config at src/lerobot/robots/utils.py:25. |
G1 driver split |
scripts/ |
first-party | 16 CLI entry-points (lerobot-train, lerobot-eval, lerobot-record, …). |
body-level edits only |
teleoperators/ |
first-party | Teleop ABC at src/lerobot/teleoperators/teleoperator.py:29; backends include keyboard/, gamepad/, phone/, homunculus/, leader arms (koch_leader/, so_leader/, omx_leader/, openarm_leader/, bi_*_leader/), unitree_g1/, reachy2_teleoperator/; factory make_teleoperator_from_config at src/lerobot/teleoperators/utils.py:36. |
G1 RemoteController grew (dual-source joystick) |
templates/ |
first-party | Single Jinja-style file lerobot_modelcard_template.md for HF Hub model cards. |
unchanged |
transport/ |
first-party | gRPC plumbing — services.proto, generated services_pb2.py, services_pb2_grpc.py, helpers in utils.py. |
byte-identical |
utils/ |
first-party | Cross-cutting helpers — constants.py, hub.py (HubMixin), import_utils.py, logging_utils.py, random_utils.py, robot_utils.py, rotation.py, train_utils.py, transition.py, visualization_utils.py, errors.py. |
small content edits in import_utils.py, io_utils.py |
src/lerobot/__init__.py is byte-identical
between the two tags — the top-level module is intentionally
lightweight and exposes only string registries plus
__version__. There is no make_* symbol exposed
from the top of the package; factories must be imported from their
submodules.
available_policies at
src/lerobot/__init__.py:160 still lists
only ["act", "diffusion", "tdmpc", "vqbet"], even though
both tags ship 14 policy folders including pi0,
pi05, pi0_fast, smolvla, groot, xvla, wall_x, sac, sarm, rtc. The
real registry is the draccus ChoiceRegistry on
PreTrainedConfig
(src/lerobot/configs/policies.py:41) —
so import lerobot; lerobot.available_policies is a
misleading artifact. Don't trust it programmatically.
Show the public factory functions (make_policy, make_dataset, …)
Every Lego piece is built by a small make_* factory.
These have to be imported from their submodules — nothing is
re-exported at the top level of the package.
| Factory | Location |
|---|---|
make_policy(...) |
src/lerobot/policies/factory.py:405 (v0.4.4: :406) |
make_policy_config(policy_type, **kwargs) |
src/lerobot/policies/factory.py:140 |
make_pre_post_processors(...) |
src/lerobot/policies/factory.py:213 |
get_policy_class(name) |
src/lerobot/policies/factory.py:60 |
make_dataset(cfg) |
src/lerobot/datasets/factory.py:71 |
make_env(...) |
src/lerobot/envs/factory.py:102 |
make_optimizer_and_scheduler(...) |
src/lerobot/optim/factory.py:25 |
make_default_processors() |
src/lerobot/processor/factory.py:58 |
make_robot_from_config(config) |
src/lerobot/robots/utils.py:25 |
make_teleoperator_from_config(config) |
src/lerobot/teleoperators/utils.py:36 |
make_cameras_from_configs(camera_configs) |
src/lerobot/cameras/utils.py:25 |
make_robot_env(cfg) (HIL-SERL gym wrapper) |
src/lerobot/rl/gym_manipulator.py:303 |
make_lerobot_observation(...) (async client) |
src/lerobot/async_inference/helpers.py:135 |
make_locomotion_controller(name) (G1) |
src/lerobot/robots/unitree_g1/g1_utils.py:66 (v0.5.0 only) |
Per-policy make_<name>_pre_post_processors(...) |
One per policy folder (act, diffusion, tdmpc, vqbet, sac, smolvla, groot, xvla, wall_x, pi0, pi05, pi0_fast, sarm) |
Device builders are named make_<X>_from_config(config)
and take an already-instantiated config object (cameras, robots,
teleoperators). Pipeline builders are named make_<X>(cfg)
and take the full pipeline config (make_dataset(cfg),
make_env(...), make_policy(...)).
There is no make_robot("ur20", **kwargs) sugar.
Anyone building a wrapper that expects that shape will rediscover
this every time.
Configuration is dataclass + draccus, not Hydra
TrainPipelineConfig at
src/lerobot/configs/train.py:37 is a
single root dataclass that holds nested configs as typed fields (no
defaults: list, no merge logic). Polymorphic dispatch is
through draccus ChoiceRegistry:
PreTrainedConfig at
src/lerobot/configs/policies.py:41 is the
registry every per-policy *Config subclasses, which is how
--policy.type=act selects the right one.
Show the v0.5.0 anatomy delta (what moved, what was added)
diff -rq across src/lerobot/ shows ~40 files
differ. Almost all are body-level edits inside policies and per-robot
drivers. The structural changes:
Removed in v0.5.0
- src/lerobot/robots/unitree_g1/robot_kinematic_processor.py (313 lines) — per-frame
WeightedMovingFilter+ IK pre-processor. - examples/unitree_g1/gr00t_locomotion.py — promoted into the library.
- examples/unitree_g1/holosoma_locomotion.py — promoted into the library.
Constraints
requires-python = ">=3.10"(pyproject.toml:32)transformers>=4.57.1,<5.0.0(pyproject.toml:102)cudnn.benchmark = Trueunconditional in scripts/lerobot_train.py:212-216
Added (structural)
- src/lerobot/policies/pi_gemma.py (363 lines) — shared Gemma / PaliGemma backbone module; first-class AdaRMS via
PiGemmaRMSNorm. - src/lerobot/robots/unitree_g1/g1_kinematics.py (287 lines) — kinematics promoted to its own module.
- src/lerobot/robots/unitree_g1/gr00t_locomotion.py (205 lines) — NVIDIA GR00T-WBC ONNX adapter.
- src/lerobot/robots/unitree_g1/holosoma_locomotion.py (214 lines) — Amazon FAR Holosoma ONNX adapter.
- tests/robots/test_unitree_g1.py + teleoperator tests.
Constraints
requires-python = ">=3.12"(pyproject.toml:32)transformers>=5.3.0,<6.0.0(pyproject.toml:102)huggingface_hub>=1.0.0,<2.0.0— major bump, extras dropped- New
cudnn_deterministicfield gates cuDNN at scripts/lerobot_train.py:212-216 - New dep:
unitree-sdk2==1.0.1(pyproject.toml:122)
- No subpackages added or removed. Same 18.
- No top-level public symbols added or removed in
lerobot/__init__.py. Byte-identical file. - No new entry-point scripts. Same 16.
- No restructuring of
configs/,processor/,transport/,datasets/,policies/factory.py,envs/factory.py, oroptim/factory.py. - No dataset schema change — both tags emit
CODEBASE_VERSION = "v3.0"(src/lerobot/datasets/lerobot_dataset.py:83).
For a UR20 + ACT/Diffusion stack, the only one of these changes that actually matters is the Python 3.12 floor. Everything else is internal to the π-family policies or the Unitree G1 driver.