
Train an Action Chunking Transformer from scratch on an SO-100 with lerobot: the act config, chunk_size and n_action_steps, the 100000 step schedule, 20 ms inference.
ACT is the odd one out among the SO-100 policies. GR00T N1.7 and N1.5 start from nvidia/GR00T-N1.7-3B and nvidia/GR00T-N1.5-3B, Pi0.5 from lerobot/pi05_base. ACT starts from nothing: there is no base checkpoint, because it is not a foundation model. It is a roughly 80 million parameter transformer you train from scratch on one task, on your arm, under your lighting.
That is also why it runs a control step in 20 ms where a 3 billion parameter VLA needs 152 to 485 ms, and costs 1 to 3 USD per run instead of 4 to 12. This guide walks the manual route with lerobot on an SO-100: record, the act config, what chunk_size and n_action_steps control, the 100000 step schedule, the rollout. Then the same job on AY-Robots, including where the platform does not help.
What you need to know
- •ACT trains from scratch: no base model, no pretraining, no language input. One checkpoint, one task.
- •The paper: about 80 M parameters, around 5 hours on an 11 GB RTX 2080 Ti, 0.01 s inference.
- •lerobot defaults: chunk_size 100, n_action_steps 100, batch 8, lr 1e-5, 100000 steps, seed 1000.
- •On AY-Robots: 20 ms per step, the fastest of the five. 50 episodes minimum, LeRobot v3.0, a 24 GB card, 1 to 3 USD per run.
- •It wins on a task it has seen, and loses the moment you want language conditioning.
Checked 23 August 2026 against lerobot 0.6.x: pyproject.toml on main reads version = "0.6.2", newest tag v0.6.1, 3 August 2026. A tutorial starting with python lerobot/scripts/train.py predates the console entry points lerobot-train, lerobot-record and lerobot-rollout.
What ACT actually is
Action Chunking with Transformers comes from the ALOHA paper, Learning Fine-Grained Bimanual Manipulation with Low-Cost Hardware, by Zhao, Kumar, Levine and Finn, arXiv, 23 April 2023. The rig records at 50 Hz with four webcams streaming 480x640 at 30 fps: two on the grippers, one top, one front. The abstract claims six skills at 80 to 90 percent success, among them opening a translucent condiment cup and slotting a battery, from 10 minutes of demonstrations.
The body is more useful when planning a recording session: 50 demonstrations per task, except Thread Velcro at 100, which is 10 to 20 minutes of data and 30 to 60 minutes of wall-clock time once resets are counted. Success is not uniform either: Thread Velcro ends at 20 percent, Put On Shoe at 92, Cup Open 84, Prep Tape 64.
| Hyperparameter | ALOHA paper, Table III | lerobot on main |
|---|---|---|
| learning rate | 1e-5 | optimizer_lr = 1e-5 |
| batch size | 8 | batch_size = 8, in TrainPipelineConfig not ACTConfig |
| encoder / decoder layers | 4 / 7 | n_encoder_layers = 4 / n_decoder_layers = 1 |
| chunk size k | 100 | chunk_size = 100 |
| latent dim of z | absent; Fig. 11 shows a 32 to 512 projection | latent_dim = 32 |
| temporal ensembling | absent; --temporal_agg in the reference code | temporal_ensemble_coeff = None |
The paper lists 7 decoder layers, lerobot ships 1, deliberately. The comment in configuration_act.py says the original implementation has a bug meaning only the first layer is used, citing issue 25 in tonyzhaozh/act: the action head reads hs[0], so all seven layers run but only the first output reaches the prediction. That issue is open and unanswered since 23 April 2024. lerobot matches the behaviour that produced the published results, not the printed number. Raise --policy.n_decoder_layers and you train a model the paper never evaluated.
Action chunking is the whole idea
Ordinary behavioural cloning maps one observation to one action, and errors compound: a deviation puts the arm off-distribution, producing a worse action, and thirty steps later the gripper is nowhere near the object. Action chunking predicts k actions at once and executes them, dropping the effective horizon by a factor of k. It also handles a nuisance specific to human data: teleoperators pause, and a single-step Markovian policy cannot model a pause that depends on what came before.
The paper ablates k rather than asserting it. With temporal ensembling off, averaged over four settings, success rises from 1 percent at k = 1 to 44 percent at k = 100, then tapers at 200 and 400 as the policy nears open-loop control. That curve is why the default is 100.
- chunk_size: how many future actions the decoder predicts per forward pass. Default 100.
- n_action_steps: how many of them you execute before querying again. Default 100, so lerobot runs the whole chunk open loop.
- lerobot validates
n_action_steps <= chunk_sizeand raises aValueErrorif you get it backwards.
What matters operationally is chunk_size divided by frame rate. At the 30 fps lerobot's SO-100 examples use, a chunk of 100 commits about 3.3 seconds from one observation. If the task needs a correction inside that window, lower n_action_steps, not chunk_size: you keep the long prediction and re-observe more often.
# predict 100 actions, re-query after 25 of them (about 0.8 s at 30 fps)
lerobot-train \
--dataset.repo_id=${HF_USER}/so100_cube \
--policy.type=act \
--policy.chunk_size=100 \
--policy.n_action_steps=25 \
--policy.device=cudaSet --policy.temporal_ensemble_coeff and lerobot requires n_action_steps = 1, raising NotImplementedError otherwise. Ensembling queries the policy every timestep and blends the overlapping predictions for that timestep with weights w_i = exp(-m * i), the oldest getting w_0. The paper puts it at 3.3 percent for ACT: real but modest, and it multiplies inference count by the chunk length. Affordable at 20 ms per step, not at 485 ms. See inference latency.
When ACT beats a foundation model
The five trainable policies side by side, with the numbers AY-Robots measures and uses to size the GPU it rents.
| Policy | Family | Params | Per step | GPU tier | Min episodes | Dataset |
|---|---|---|---|---|---|---|
| ACT | Chunking transformer, from scratch | ~80 M | 20 ms | RTX 4090 / 24 GB | 50 | LeRobot v3.0 |
| SmolVLA | Compact VLA | ~450 M | 245 ms | RTX 4090 / 24 GB | 30 | LeRobot v3.0 |
| GR00T N1.7 | VLA foundation, diffusion head | ~3 B (~40 M trained) | 152 ms | A100 / H100 80 GB | 50 | LeRobot v2.0 or v2.1 |
| GR00T N1.5 | VLA foundation, predecessor | ~3 B | 165 ms | A100 / H100 80 GB | 50 | LeRobot v2.0 or v2.1 |
| Pi0.5 | Flow-matching VLA, see flow matching | ~3 B, PaliGemma backbone | 485 ms | A100 / H100 80 GB | 50 | LeRobot v3.0 |

- 20 ms per action step, the fastest of the five, on a 24 GB card not an A100.
- 1 to 3 USD per run against 4 to 12 for the 3 B class.
- Precise on contact-rich work it has seen: 88 and 96 percent on Slide Ziploc and Slot Battery, where prior methods never cleared stage one.
- No language conditioning: the task string is ignored, so one checkpoint is one task.
- No semantic priors: everything it knows came from your 50 episodes.
- Narrow generalisation: move a camera and you are retraining.
- It fails quietly: loss falls, the arm does nothing, the logs say nothing.
- The speed advantage only helps if inference sits next to the servos.
Pick ACT when task and scene are fixed and motion must be fast and precise. Pick a vision-language-action model when one checkpoint must cover several instructions. Two pages work the decision head to head: ACT vs SmolVLA and ACT vs GR00T N1.7. For published benchmarks, the ACT arena entry links every number to its source.
What you need before you start
One follower arm, one leader arm for teleoperation, at least one camera, a 24 GB GPU. ACT reads images and joint positions only. Two cameras is the sweet spot: a fixed front view for where things are, a wrist camera for what the end effector is about to touch, as on ALOHA.
| Arm | Servos | Voltage | Parts cost | Status |
|---|---|---|---|---|
| SO-100 | Feetech STS3215 | 7.4 V | ~110 to 150 EUR | Full support, reference arm |
| SO-101 | Feetech STS3215 | 7.4 V | ~130 to 170 EUR | Full support |
| Koch v1.1 | Dynamixel XL330 / XL430 | 5 V and 12 V rails | ~250 to 350 EUR | Compatible |
| LeKiwi | Feetech STS3215 (arm) | 7.4 V arm, 12 V base | ~400 to 500 EUR | Compatible |
SO-100 and SO-101 run Feetech STS3215 servos on a 7.4 V rail. Feeding them 12 V destroys them, quietly enough that people blame the software first, and a Koch 12 V supply physically fits an SO-100 board. Check the label. Symptoms: servo not responding, arm twitches then sags. Also SO-100 vs SO-101.
From bare arm to recorded dataset
The flow below is lerobot 0.6.x. Skip it if you have a calibrated arm and a dataset. Otherwise the SO-100 getting started guide covers assembly, the recording walkthrough covers capture and the dataset docs the format.
- 1Install lerobot with the right extras
Recording needs
core_scripts, trainingtraining, Feetech servosfeetech. Python 3.12+.bashconda create -y -n lerobot python=3.12 conda activate lerobot conda install ffmpeg -c conda-forge pip install 'lerobot[core_scripts,training,feetech]' lerobot-info - 2Find the USB port of each arm
Run it with both arms plugged in, unplugging one when prompted. On Linux you may need to open the node permissions.
bashlerobot-find-port # on Linux, if the port exists but is unreadable: sudo chmod 666 /dev/ttyACM0 - 3Set the motor ids and baudrate
On the SO-100 this happens before assembly: unlike the SO-101 the connectors are unreachable once built. The script walks the bus one motor at a time from the gripper, writing ids to EEPROM.
bashlerobot-setup-motors \ --robot.type=so100_follower \ --robot.port=/dev/ttyACM0 lerobot-setup-motors \ --teleop.type=so100_leader \ --teleop.port=/dev/ttyACM1 - 4Calibrate both arms
Set every joint to the middle of its range, press Enter, then sweep each through its full range. Calibration lets a policy trained on one arm run on another. Reuse the same
id.bashlerobot-calibrate \ --robot.type=so100_follower \ --robot.port=/dev/ttyACM0 \ --robot.id=my_follower lerobot-calibrate \ --teleop.type=so100_leader \ --teleop.port=/dev/ttyACM1 \ --teleop.id=my_leader - 5Teleoperate once with the cameras on
The lerobot rule of thumb: you should be able to do the task looking only at the camera images. If you cannot, neither can ACT. This catches more bad datasets than later debugging.
bashlerobot-teleoperate \ --robot.type=so100_follower \ --robot.port=/dev/ttyACM0 \ --robot.id=my_follower \ --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30} }" \ --teleop.type=so100_leader \ --teleop.port=/dev/ttyACM1 \ --teleop.id=my_leader \ --display_data=true - 6Record 50 episodes
50 is the AY-Robots minimum and what ALOHA used per task. lerobot advises 10 per object location, cameras fixed, grasp consistent.
nends an episode,rre-records,qstops and encodes.bashHF_USER=$(NO_COLOR=1 hf auth whoami | awk -F': *' 'NR==1 {print $2}') lerobot-record \ --robot.type=so100_follower \ --robot.port=/dev/ttyACM0 \ --robot.id=my_follower \ --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30} }" \ --teleop.type=so100_leader \ --teleop.port=/dev/ttyACM1 \ --teleop.id=my_leader \ --dataset.repo_id=${HF_USER}/so100_cube \ --dataset.num_episodes=50 \ --dataset.single_task="Grab the black cube and put it in the bin" \ --display_data=true

ACT has no priors to fall back on, so every inconsistency becomes permanent. The three costliest: a camera nudged between episode 20 and 21, light that changed because you recorded half the set in the afternoon, a grasp done two ways. Each gives a perfect-looking loss curve and an arm that goes to the wrong place. See loss falls, policy does nothing, policy only works in one setup and collecting high quality training data.
Before training, replay at least five episodes. The LeRobot dataset format stores camera streams, joint states and actions per episode, and replay pushes those actions back at the arm. If the replay does not do the task, the data does not contain it and training will not invent it.
lerobot-replay \
--robot.type=so100_follower \
--robot.port=/dev/ttyACM0 \
--robot.id=my_follower \
--dataset.repo_id=${HF_USER}/so100_cube \
--dataset.episode=0Training the ACT policy
This is the whole command. Everything ACT-specific is already a default, which is why the lerobot ACT page says to start with them.
lerobot-train \
--dataset.repo_id=${HF_USER}/so100_cube \
--policy.type=act \
--output_dir=outputs/train/act_so100_cube \
--job_name=act_so100_cube \
--policy.device=cuda \
--wandb.enable=true \
--policy.repo_id=${HF_USER}/act_so100_cube--policy.type=act loads ACTConfig, which adapts to however many motors and cameras your dataset recorded, so you never declare the observation shape. --wandb.enable=true is optional and worth it: the loss curve is the only cheap signal in a 100000 step run. The schedule comes from lerobot's train config, not ACTConfig: 100000 steps, batch 8, seed 1000, a checkpoint every 20000 steps, logging every 200.
A full run leaves five checkpoint directories, 020000 through 100000, plus a last symlink. Keep them all: the best policy is often not the last one.
| Setting | lerobot default | AY-Robots ACT form | Comment |
|---|---|---|---|
| batch size | 8 | 8 | Lower it first if you hit VRAM limits. |
| learning rate | 1e-5 | 1e-5 | Same as the ALOHA paper. |
| max steps | 100000 | 100000 | Roughly where a 50 episode set stops improving. |
| gradient accumulation | 1 | 1, does not apply | Change batch size instead. |
| seed | 1000 | exposed | GR00T's tyro entry point has no seed; ACT runs are the reproducible ones. |
| chunk_size / n_action_steps | 100 / 100 | 100 / 100, editable | Prediction and execution horizon. Lower the second, not the first. |
| checkpoint frequency | 20000 | not exposed | saveSteps is a GR00T knob here. |
ACT at batch size 8 with two 640x480 cameras fits comfortably on 24 GB. It stops fitting when people raise the batch size for speed, or feed it the 1920x1080 frames one lerobot recording example shows. Two ResNet-18 backbones at 1080p is a very different memory profile. Drop --batch_size to 4 before renting a bigger card. See out of memory in training.
Duration: around 5 hours on an 11 GB RTX 2080 Ti in the paper, a few hours for 100k steps per lerobot's ACT page, 2 to 5 hours on the AY-Robots 24 GB tier. Do not cut it short. The reference repo's README says a jerky or pausing policy usually just needs more training, because success and smoothness keep improving after the loss plateaus: for real-world data it wants at least 5000 epochs, or 3 to 4 times the length again after the plateau.
lerobot-train \
--config_path=outputs/train/act_so100_cube/checkpoints/last/pretrained_model/train_config.json \
--resume=trueRunning the trained policy on the arm
Deployment uses lerobot-rollout. Camera keys must match the recorded ones: a policy trained on front and wrist will not accept cam0 and cam1, and rename_map does not help, since it needs a pretrained checkpoint. The task string can be omitted; lerobot's own example marks it skippable for ACT.
lerobot-rollout \
--strategy.type=base \
--policy.path=${HF_USER}/act_so100_cube \
--robot.type=so100_follower \
--robot.port=/dev/ttyACM0 \
--robot.id=my_follower \
--robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30} }" \
--display_data=true \
--duration=60Evaluation used to run through lerobot-record --policy.path=.... In 0.6.x it is lerobot-rollout with a --strategy.type selector: base, sentry (recording with auto-upload), highlight (ring buffer saved by keystroke), dagger (human in the loop) and episodic. As of 23 August 2026 the ACT documentation page still says "using the lerobot-record command" directly above a block that runs lerobot-rollout. Follow the command, not the sentence.
To pin a checkpoint rather than the final model, add --policy.pretrained_revision. That needs the run to have started with --save_checkpoint_to_hub=true, off by default: without it lerobot pushes the final model and nothing else. With it, each checkpoint is tagged with its zero-padded step, so --policy.pretrained_revision=060000 recovers the 60000 step one. Comparing it against 100000 on the real arm is the cheapest experiment available.
Two routes to the same checkpoint
Everything above is the manual route and it works. The platform route trades control for not owning a GPU or a Python environment.
- Install lerobot 0.6.x with
core_scripts,training,feetech, ffmpeg. - Find ports, set motor ids, calibrate both arms, record 50 episodes.
- Replay a few episodes to confirm the data contains the task.
- Rent or own a 24 GB GPU, match CUDA and PyTorch, run
lerobot-train --policy.type=act. - Wait a few hours, then
lerobot-rollouton the machine at the arm.
# the two commands that matter, end to end
lerobot-record --robot.type=so100_follower --robot.port=/dev/ttyACM0 \
--teleop.type=so100_leader --teleop.port=/dev/ttyACM1 \
--dataset.repo_id=${HF_USER}/so100_cube --dataset.num_episodes=50 \
--dataset.single_task="Grab the black cube"
lerobot-train --dataset.repo_id=${HF_USER}/so100_cube --policy.type=act \
--output_dir=outputs/train/act_so100_cube --policy.device=cudaTotal control: edit configuration_act.py, add a camera, fork the trainer. For research rather than shipping a task, a platform is a distraction.
- Record with the desktop client, or bring a Hugging Face repo id or local dataset.
- Open the ACT on SO-100 guide and pick model and dataset. Defaults are the lerobot ones; chunkSize, nActionSteps, seed and logFreq are editable.
- The backend rents a GPU sized by VRAM and writes checkpoints to object storage.
/api/inference/podthen serves the policy to the local robot client. An idle watchdog destroys the pod, so nothing bills silently.- The same operations exist in the CLI, the MCP server and the training docs.
It does not fix your data: a dataset with a moved camera trains exactly as badly here, and the form cannot detect it. Nor does it remove the latency problem. The control loop is 20 to 485 ms per action step, with public-internet round trips on top, and ACT is hurt most because its step is shortest: 60 ms is a 12 percent slowdown on Pi0.5's 485 ms but four times the step on ACT's 20 ms. Remote inference suits slow pick and place, not fast reactive motion.

What actually goes wrong
Almost none of the pain is in the training command. It is in the things around it, ordered by how often they bite first time.
| Symptom | Usual cause | Page |
|---|---|---|
| lerobot-find-port shows nothing | Driver, cable or node permissions | arm not detected |
| Camera missing at record time | Index changed on reboot, or two cameras on one USB controller | camera not detected |
| Training rejects the dataset | ACT wants v3.0, GR00T needs v2.1 | dataset rejected as v3 |
| CUDA out of memory | Batch size raised, or 1080p frames instead of 480p | out of memory in training |
| Loss looks great, arm does nothing | The data lacks the task, or a camera moved | loss falls, policy does nothing |
| Jerky motion or a pause mid-episode | Undertrained, a chunk boundary stall, or a timed-out inference call | policy freezes mid-motion |
Two rows deserve emphasis. ACT trains on LeRobot v3.0 while GR00T's loader crashes on it and needs v2.1, so a dataset that trains ACT can fail a GR00T run. And the last row has two fixes: the ACT authors answer jerky motion with more training, while with n_action_steps at 100 a genuine stall lands at a chunk boundary, a visible pause every 3.3 seconds at 30 fps. Two more to know: a single dead joint is usually a servo id that was never written, and a gripper that approaches but never closes means too little gripper range in the demonstrations. Full index: the failure mode pages.
What a run costs
| Tier | Models | Run time | Price per hour | Cost per run |
|---|---|---|---|---|
| RTX 4090 / 24 GB | ACT, SmolVLA | 2 to 5 hours | 0.30 to 0.60 USD | about 1 to 3 USD |
| A100 80 GB / H100 | GR00T N1.7, GR00T N1.5, Pi0.5 | 3 to 6 hours | 1.20 to 2.00 USD | about 4 to 12 USD |
This is the argument for starting with ACT even if you want a VLA later. A failed ACT run costs the price of a coffee and tells you within hours whether your dataset contains the task. A failed GR00T run costs four times that for the same lesson. Moving up to GR00T N1.7 or SmolVLA afterwards is a form change, not a rebuild. Background: vision-language-action models, the complete SO-100 guide, train your first policy and imitation learning. No arm? The live page streams a real SO-100 to drive without signing up.
Train ACT on your SO-100
The guide for this exact combination: defaults, GPU tier and what a run costs. Pick the dataset, the backend rents the card and writes the checkpoints.
Open the training guideIs there a pretrained ACT model I can fine-tune instead?▾
No. ACT has no base model; it only exists after you train it. That is not a gap in the tooling, it is what ACT is: the paper trains a policy from scratch per task. For a vendor checkpoint, use GR00T N1.7 or Pi0.5.
How many episodes do I really need?▾
50: what ALOHA recorded per task (100 for Thread Velcro, its hardest) and the AY-Robots minimum. lerobot advises about 10 per object location, cameras fixed, grasp consistent. Fifty clean episodes beat a hundred where the camera moved.
Should I change chunk_size from 100?▾
Usually not. The ablation climbs from 1 percent at k = 1 to 44 percent at k = 100 and tapers after, so 100 sits near the top. If the arm commits too long, lower n_action_steps instead: at 30 fps, 25 re-queries every 0.8 seconds.
How long does a training run take, and can I stop it early?▾
Two to five hours on a 24 GB card for 100000 steps. Checkpoints land every 20000 steps and --resume=true picks a run back up, so stopping early is safe. Just not at the first flat stretch: smoothness improves after the loss plateaus.
Loss went down and the arm still fails. What now?▾
Nearly always the dataset. Replay recorded episodes at the arm: if the replay does not do the task, the data does not contain it. Then check whether anything moved, especially a camera. ACT has no priors, so a nudge on episode 21 is permanent.
Sources
- Zhao, Kumar, Levine, Finn: Learning Fine-Grained Bimanual Manipulation with Low-Cost Hardware (ACT), arXiv 2304.13705
- ALOHA / ACT project page
- tonyzhaozh/act, the reference implementation and its training-length advice
- tonyzhaozh/act issue 25: the action head reads only the first decoder layer
- huggingface/lerobot
- lerobot ACTConfig: chunk_size, n_action_steps, n_decoder_layers and the rest of the defaults
- lerobot TrainPipelineConfig: steps, batch_size, seed, save_freq, log_freq, save_checkpoint_to_hub
- lerobot train_utils: zero-padded checkpoint dirs, the last symlink and checkpoint push tagging
- lerobot v0.6.1 release, 3 August 2026
- LeRobot docs: ACT
- LeRobot docs: imitation learning on real robots (record, replay, train, rollout)
- LeRobot docs: SO-100 setup, motor ids and calibration
- LeRobot docs: installation and the optional extras
- Hugging Face blog: LeRobot Community Datasets, the ImageNet of Robotics, When and How?
- TheRobotStudio/SO-ARM100: Standard Open Arm 100
Ready for high-quality robotics data?
AY-Robots connects your robots to skilled operators worldwide.
Get Started