The AY-Robots training matrix with five policy rows and four robot arm columns, each cell linking to a specific model and arm training guide
ACTSO-100lerobotimitation learningpolicy training

How to Train ACT on the SO-100 from Scratch

AY-Robots ResearchAugust 23, 202616 min read

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.
Which versions this describes

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.

HyperparameterALOHA paper, Table IIIlerobot on main
learning rate1e-5optimizer_lr = 1e-5
batch size8batch_size = 8, in TrainPipelineConfig not ACTConfig
encoder / decoder layers4 / 7n_encoder_layers = 4 / n_decoder_layers = 1
chunk size k100chunk_size = 100
latent dim of zabsent; Fig. 11 shows a 32 to 512 projectionlatent_dim = 32
temporal ensemblingabsent; --temporal_agg in the reference codetemporal_ensemble_coeff = None
The decoder layer row is not a typo

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_size and raises a ValueError if 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.

bash
# 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=cuda
Shortening the executed part of the chunk without shortening the prediction.
The temporal ensembling trap

Set --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.

PolicyFamilyParamsPer stepGPU tierMin episodesDataset
ACTChunking transformer, from scratch~80 M20 msRTX 4090 / 24 GB50LeRobot v3.0
SmolVLACompact VLA~450 M245 msRTX 4090 / 24 GB30LeRobot v3.0
GR00T N1.7VLA foundation, diffusion head~3 B (~40 M trained)152 msA100 / H100 80 GB50LeRobot v2.0 or v2.1
GR00T N1.5VLA foundation, predecessor~3 B165 msA100 / H100 80 GB50LeRobot v2.0 or v2.1
Pi0.5Flow-matching VLA, see flow matching~3 B, PaliGemma backbone485 msA100 / H100 80 GB50LeRobot v3.0
The AY-Robots policies page showing a comparison table of ACT, SmolVLA, GR00T N1.5, GR00T N1.7 and Pi0.5 with parameter counts, GPU requirements, inference latency and minimum episode counts
The /policies table: ACT has the smallest parameter count and the shortest step time.
Training ACT from scratch
Advantages
  • 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.
Trade-offs
  • 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.

ArmServosVoltageParts costStatus
SO-100Feetech STS32157.4 V~110 to 150 EURFull support, reference arm
SO-101Feetech STS32157.4 V~130 to 170 EURFull support
Koch v1.1Dynamixel XL330 / XL4305 V and 12 V rails~250 to 350 EURCompatible
LeKiwiFeetech STS3215 (arm)7.4 V arm, 12 V base~400 to 500 EURCompatible
7.4 V, not 12 V

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.

  1. 1
    Install lerobot with the right extras

    Recording needs core_scripts, training training, Feetech servos feetech. Python 3.12+.

    bash
    conda 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
  2. 2
    Find 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.

    bash
    lerobot-find-port
    # on Linux, if the port exists but is unreadable:
    sudo chmod 666 /dev/ttyACM0
  3. 3
    Set 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.

    bash
    lerobot-setup-motors \
        --robot.type=so100_follower \
        --robot.port=/dev/ttyACM0
    
    lerobot-setup-motors \
        --teleop.type=so100_leader \
        --teleop.port=/dev/ttyACM1
  4. 4
    Calibrate 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.

    bash
    lerobot-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
  5. 5
    Teleoperate 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.

    bash
    lerobot-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
  6. 6
    Record 50 episodes

    50 is the AY-Robots minimum and what ALOHA used per task. lerobot advises 10 per object location, cameras fixed, grasp consistent. n ends an episode, r re-records, q stops and encodes.

    bash
    HF_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
The AY-Robots recording tutorial page explaining how to capture a LeRobot-format dataset from a teleoperation session
The recording tutorial. The desktop client writes the same layout as lerobot-record.
The trap that eats a day: an inconsistent dataset

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.

bash
lerobot-replay \
    --robot.type=so100_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.id=my_follower \
    --dataset.repo_id=${HF_USER}/so100_cube \
    --dataset.episode=0
Replaying episode 0. If this fails, stop and re-record.

Training 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.

bash
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
The training command from the lerobot 0.6.x docs, on an SO-100 dataset.

--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.

Settinglerobot defaultAY-Robots ACT formComment
batch size88Lower it first if you hit VRAM limits.
learning rate1e-51e-5Same as the ALOHA paper.
max steps100000100000Roughly where a 50 episode set stops improving.
gradient accumulation11, does not applyChange batch size instead.
seed1000exposedGR00T's tyro entry point has no seed; ACT runs are the reproducible ones.
chunk_size / n_action_steps100 / 100100 / 100, editablePrediction and execution horizon. Lower the second, not the first.
checkpoint frequency20000not exposedsaveSteps is a GR00T knob here.
Out of memory is a batch size problem, not a card problem

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.

bash
lerobot-train \
  --config_path=outputs/train/act_so100_cube/checkpoints/last/pretrained_model/train_config.json \
  --resume=true
Resuming an interrupted run from its last checkpoint.

Running 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.

bash
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=60
Autonomous rollout with no recording. Use --strategy.type=sentry to record the evaluation episodes.
This command was renamed recently, so old tutorials disagree

Evaluation 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.

  1. Install lerobot 0.6.x with core_scripts, training, feetech, ffmpeg.
  2. Find ports, set motor ids, calibrate both arms, record 50 episodes.
  3. Replay a few episodes to confirm the data contains the task.
  4. Rent or own a 24 GB GPU, match CUDA and PyTorch, run lerobot-train --policy.type=act.
  5. Wait a few hours, then lerobot-rollout on the machine at the arm.
bash
# 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=cuda
What this route gives you

Total control: edit configuration_act.py, add a camera, fork the trainer. For research rather than shipping a task, a platform is a distraction.

The AY-Robots training matrix with five policy rows and four robot arm columns, where every cell links to the specific training guide for that model and arm combination
The matrix on /train. The ACT row against the SO-100 column is this article's guide.

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.

SymptomUsual causePage
lerobot-find-port shows nothingDriver, cable or node permissionsarm not detected
Camera missing at record timeIndex changed on reboot, or two cameras on one USB controllercamera not detected
Training rejects the datasetACT wants v3.0, GR00T needs v2.1dataset rejected as v3
CUDA out of memoryBatch size raised, or 1080p frames instead of 480pout of memory in training
Loss looks great, arm does nothingThe data lacks the task, or a camera movedloss falls, policy does nothing
Jerky motion or a pause mid-episodeUndertrained, a chunk boundary stall, or a timed-out inference callpolicy 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

TierModelsRun timePrice per hourCost per run
RTX 4090 / 24 GBACT, SmolVLA2 to 5 hours0.30 to 0.60 USDabout 1 to 3 USD
A100 80 GB / H100GR00T N1.7, GR00T N1.5, Pi0.53 to 6 hours1.20 to 2.00 USDabout 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 guide
Is 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.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started