Reproducing SmolVLA on 16 LeRobot v3 datasets spanning four robot platforms — bimanual ALOHA (sim + real), single-arm xArm, and the 2-DOF PushT planar pusher — all on a Mac (Apple MPS, no NVIDIA GPU).
Companion repo to SmolVLA_cl (LIBERO + EEG modality experiments). Where the cl repo asks "can we add a new modality?", this repo asks "how well does SmolVLA generalise across embodiments and data distributions?"
All 16 SmolVLA checkpoints and the 5 RT-1 checkpoints are published on HuggingFace:
Twu31/smolvla-cross-embodiment-mps— one checkpoint per dataset, withdataset_stats.jsonand the eval.npz/.pngbehind every number in the tables belowTwu31/rt1-lerobot-mps— the RT-1 arm of the head-to-head
Note the model cards are more precise than this README on one point: these runs use
train_expert_only=False, so only the vision tower is frozen — the VLM text tower trains
too. "SmolVLM2 frozen" below should be read as "frozen vision encoder".
Architecture > Scale > Data Quantity. At small-data behavior-cloning scale (50–800 episodes), the choice of action head (continuous flow-matching vs. discrete 256-bin tokens) and the choice of pretrained backbone matter more than the parameter count. A 243M-parameter RT-1 beats a 450M-parameter SmolVLA on 3 of 5 benchmarked tasks.
Seven concrete lessons distilled from 16 SmolVLA runs + 5 RT-1 runs on the same Mac MPS:
-
Real-robot data is no harder than sim — Top-4 real ALOHA datasets (ziploc 0.556, battery 0.651, cups 0.694, coffee 0.802) outperform sim ALOHA (insertion 0.716, transfer 0.729). The sim-to-real gap is overhyped for behavior cloning.
-
Architecture choice flips with task difficulty — RT-1's discrete actions win by −19% to −43% on stereotyped tasks (transfer, battery, push), but lose by +43% on hard ones (xarm_lift). SmolVLA's pretrained SmolVLM2 backbone is the deciding factor when tasks are hard, not the action head.
-
Scripted demos overfit harder than human —
aloha_insertion_scriptedreaches train loss 0.040 (vs human 0.118) but evaluates worse (L2 0.864 vs 0.716). Human inconsistency acts as regularization. -
Image resolution is a silent bottleneck —
xarm_push(84×84, no gripper) is the best dataset overall (L2 0.216);xarm_lift(84×84, with gripper) is one of the worst (L2 1.324). Same image, vastly different difficulty — 84×84 can't time the gripper's open/close transitions. -
Multi-task pays a ~2× penalty at small data —
aloha_multitaskL2=1.33 vs ~0.72 single-task. Shared representations need >50 episodes per task to break even. -
Normalization mismatch is a real bug —
aloha_multitask_scriptedfailed catastrophically (loss 9–55) because two scripted datasets had std-of-joint-0 differing by 40×. Always verify per-dataset stats compatibility before joint training. -
The gripper-binary problem is architecture-independent — Both RT-1 and SmolVLA fail on the right-arm gripper dim (MAE 0.13–0.27 for both). This is a data-side bottleneck — open/close transitions are 1% of frames — and no amount of model capacity fixes it.
Methodological win: All 21 model trainings ran on a single M5 MacBook (no NVIDIA GPU, ~20h total compute). VLA research has a lower entry barrier than commonly assumed.
| Rank | Dataset | L2 ↓ | MAE ↓ | Notes |
|---|---|---|---|---|
| 🥇 | xarm_push |
0.216 | 0.105 | Simplest task — 3-DOF position, no gripper |
| 🥈 | xarm_push_replay |
0.529 | 0.269 | Replay variant |
| 🥉 | aloha_static_ziploc_slide ★REAL |
0.556 | 0.106 | Best real-robot result — fine manipulation |
| 4 | aloha_static_battery ★REAL |
0.651 | 0.121 | Real-robot, battery insertion |
| 5 | aloha_static_cups_open ★REAL |
0.694 | 0.139 | Real-robot, cup opening |
| 6 | aloha_insertion |
0.716 | 0.154 | Sim, human demos |
| 7 | aloha_transfer |
0.729 | 0.149 | Sim, human demos |
| 8 | aloha_static_coffee ★REAL |
0.802 | 0.168 | Real-robot |
| 9 | aloha_insertion_scripted |
0.864 | 0.186 | Low train loss (0.040), higher L2 |
| 10 | xarm_lift_replay |
1.042 | 0.448 | 84×84 imgs, grip struggles |
| 11 | aloha_transfer_scripted |
1.156 | 0.214 | Scripted |
| 12 | xarm_lift |
1.324 | 0.565 | ❌ 84×84 resolution bottleneck |
| 13 | aloha_multitask |
1.327 | 0.276 | 2-task human, shared repr. penalty |
| 14 | aloha_static_towel ★REAL |
1.479 | 0.250 | ❌ Deformable object, hardest real-robot |
| 15 | aloha_multitask_scripted |
1.531 | 0.282 | |
| — | pusht |
40.4 px | 25.7 px | ‡ 2-DOF pixel space, not comparable |
‡ pusht L2 is in pixel units (512×512 canvas); 40 px ≈ 8% of image width per step — reasonable for flow matching.
⚠️ aloha_multitask_scripted training loss (~9–55) signals a normalization failure:transfer_scriptedhas std[joint0]=0.004 (joint locked) whileinsertion_scriptedhas std[joint0]=0.161 — applying transfer stats to insertion data inflates z-scores by 40×. Eval result not meaningful for comparison.
Seven empirical findings, each backed by at least one pair of runs:
| Finding | Evidence |
|---|---|
| Real-robot data is no harder than sim | Top-4 real-robot datasets (ziploc 0.556, battery 0.651, cups 0.694) all beat sim ALOHA (0.716–0.729) |
| Fine manipulation generalises well | aloha_static_ziploc_slide L2=0.556 is #3 overall despite precise fine-grained contact |
| Deformable objects are harder | aloha_static_towel L2=1.479 is the hardest real-robot run — deformable cloth requires reasoning about visual state that doesn't follow rigid-body heuristics |
| Scripted demos overfit differently | aloha_insertion_scripted: 3× lower train loss (0.040) but higher open-loop L2 (0.864 vs 0.716) than human demos |
| Image resolution matters for grasping | xarm_lift (84×84) L2=1.32 vs xarm_push (no gripper) L2=0.22 — same image size, vastly different difficulty |
| Multi-task pays ~2× penalty | aloha_multitask L2=1.33 vs single-task ~0.72, even at 5000 steps |
| Scripted multi-task fails via normalization mismatch | aloha_multitask_scripted loss 9–55 (vs 0.456 for human multitask) — scripted policies lock joints differently per task, making cross-dataset z-scoring degenerate |
All datasets are in LeRobot v3 format (Parquet files + MP4 videos), freely available on HuggingFace under the lerobot/ organisation.
| Group | Dataset | HF Repo | Episodes |
|---|---|---|---|
| Sim, human demos | aloha_transfer |
lerobot/aloha_sim_transfer_cube_human |
50 |
| Sim, human demos | aloha_insertion |
lerobot/aloha_sim_insertion_human |
50 |
| Sim, human demos | aloha_multitask |
both of the above | 100 |
| Sim, scripted | aloha_transfer_scripted |
lerobot/aloha_sim_transfer_cube_scripted |
50 |
| Sim, scripted | aloha_insertion_scripted |
lerobot/aloha_sim_insertion_scripted |
50 |
| Sim, scripted | aloha_multitask_scripted |
both scripted datasets | 100 |
| ★ Real robot | aloha_static_coffee |
lerobot/aloha_static_coffee |
~50 |
| ★ Real robot | aloha_static_battery |
lerobot/aloha_static_battery |
~50 |
| ★ Real robot | aloha_static_cups_open |
lerobot/aloha_static_cups_open |
50 |
| ★ Real robot (deformable) | aloha_static_towel |
lerobot/aloha_static_towel |
50 |
| ★ Real robot (fine) | aloha_static_ziploc_slide |
lerobot/aloha_static_ziploc_slide |
56 |
| Group | Dataset | HF Repo | Action dim |
|---|---|---|---|
| Lift task | xarm_lift |
lerobot/xarm_lift_medium |
4 (x,y,z,gripper) |
| Push task | xarm_push |
lerobot/xarm_push_medium |
3 (x,y,z) |
| Lift replay | xarm_lift_replay |
lerobot/xarm_lift_medium_replay |
4 |
| Push replay | xarm_push_replay |
lerobot/xarm_push_medium_replay |
3 |
| Dataset | HF Repo | Episodes | Action dim | Units |
|---|---|---|---|---|
pusht |
lerobot/pusht |
206 | 2 (x,y pixel) | pixels (0–512) |
Full configs live in dataset_configs.py.
ALOHA Static (real robot) — aloha_static_coffee, aloha_static_battery, aloha_static_cups_open, aloha_static_towel, aloha_static_ziploc_slide
INPUT
observation.images.cam_high (3, 480, 640) ← overhead RGB, normalized to [0,1]
observation.state (14,) ← 14-dim joint positions, z-score normalized
OBS_LANGUAGE_TOKENS (48,) ← tokenized task description
e.g. "Place the coffee capsule inside..."
OUTPUT
action chunk (50, 14) ← future 1 second of joint targets @ 50 FPS
z-score normalized; ×std+mean at inference
INPUT
observation.image (3, 84, 84) ← single camera
observation.state (4,) ← (x, y, z, gripper)
OBS_LANGUAGE_TOKENS (48,) ← e.g. "Lift the red cube"
OUTPUT
action chunk (10, 4 or 3) ← future 0.67 s @ 15 FPS
INPUT
observation.image (3, 96, 96) ← top-down camera, pixel coords
observation.state (2,) ← agent (x, y) in pixels
OBS_LANGUAGE_TOKENS (48,) ← "Push the T-shaped block onto the target."
OUTPUT
action chunk (10, 2) ← next 1 s @ 10 FPS, pixel coordinates
Note: L2 metric is in pixels, not radians
image → SmolVLM2 vision encoder (resize to 512×512, frozen)
│
▼
language tokens → SmolVLM2 transformer → context tokens
│
state ──────────────────┼──→ Flow-matching action expert (~100M params, trained)
▼
10-step denoising → action chunk
cd /Users/r/Projects/SmolVLA_ALOHA
# All scripts use the lerobot conda env:
PY=/opt/anaconda3/envs/lerobot/bin/python
# ── 1) Train one dataset (auto-downloads to ./Data on first run) ──────────────
DATASET=aloha_transfer TRAIN_STEPS=3000 BATCH_SIZE=4 $PY train_generic.py
DATASET=aloha_static_coffee TRAIN_STEPS=3000 BATCH_SIZE=4 $PY train_generic.py # real-robot
DATASET=xarm_push TRAIN_STEPS=3000 BATCH_SIZE=8 $PY train_generic.py
DATASET=pusht TRAIN_STEPS=3000 BATCH_SIZE=16 $PY train_generic.py
DATASET=aloha_multitask TRAIN_STEPS=5000 BATCH_SIZE=4 $PY train_generic.py
# ── 2) Open-loop evaluation (no simulator needed) ────────────────────────────
DATASET=aloha_transfer STEP=3000 N_EVAL_EP=10 $PY eval_generic.py
DATASET=aloha_static_coffee STEP=3000 N_EVAL_EP=10 $PY eval_generic.py
DATASET=pusht STEP=3000 N_EVAL_EP=20 $PY eval_generic.py
# ── 3) Cross-dataset comparison plot (once all evals are done) ───────────────
$PY plot_comparison.pyAny dataset key in dataset_configs.py is valid. To add a new LeRobot v3 dataset, append an entry to that registry and the generic train/eval scripts will handle it automatically.
| File | Purpose |
|---|---|
dataset_configs.py |
Registry of 17 dataset configurations — image keys, state/action dims, chunk size, FPS, train_steps, batch_size. Includes raw_gt flag for datasets that store raw (non-z-scored) parquet values (e.g. pusht pixel coordinates). |
train_generic.py |
Generic trainer for any dataset in the registry — handles parquet loading, MP4 video decoding by global frame index, normalization stats, action-chunking, and flow-matching loss |
eval_generic.py |
Open-loop evaluation: feeds real observations frame-by-frame, calls model.select_action() each step, computes per-dim MAE + overall L2 in original units; saves eval_{dataset}_step{step}.npz and a per-dim bar chart |
plot_comparison.py |
Reads all eval_*.npz and produces the 4-panel cross-dataset figure (L2 bar, MAE bar, normalised heatmap, loss curves) |
download_data.py |
Stand-alone HuggingFace dataset downloader (rarely needed — training auto-downloads) |
aloha_config.py |
Legacy ALOHA-specific config — superseded by the registry in dataset_configs.py but kept for reference |
train_smolvla_aloha.py |
Legacy ALOHA-only trainer; superseded by train_generic.py |
eval_smolvla_aloha.py |
Legacy ALOHA-only evaluator; superseded by eval_generic.py |
Apple MPS on M-series Mac; reuses the lerobot conda env from the parent project:
# Same env as SmolVLA_cl
PYTHONPATH is NOT required for this repo (we don't depend on LIBERO).Dependencies are inherited from a local lerobot source install at /Users/r/lerobot/src (set via sys.path.insert in train_generic.py). If you don't have that, swap it for a pip install lerobot and adjust the path.
pip install lerobot transformers torch pandas pyarrow opencv-python matplotlib huggingface_hubSmolVLA uses action chunking — one VLM forward pass generates 10–50 actions, executed one per step.
| Run | Steps | Time | Final loss |
|---|---|---|---|
aloha_transfer (sim, human) |
3000 | ~28 min | 0.118 |
aloha_insertion_scripted |
3000 | ~30 min | 0.040 |
aloha_transfer_scripted |
3000 | ~30 min | 0.060 |
aloha_multitask |
5000 | ~50 min | 0.456 |
aloha_static_coffee ★REAL |
3000 | ~35 min | 0.110 |
aloha_static_battery ★REAL |
3000 | ~33 min | 0.115 |
xarm_lift |
3000 | ~20 min | 1.502 |
xarm_push |
3000 | ~47 min | 0.300 |
xarm_lift_replay |
3000 | ~48 min | 1.513 |
xarm_push_replay |
3000 | ~48 min | 1.457 |
pusht |
3000 | ~85 min | 0.239 |
aloha_static_cups_open ★REAL |
3000 | ~33 min | — |
aloha_static_towel ★REAL |
3000 | ~25 min | — |
aloha_static_ziploc_slide ★REAL |
3000 | ~25 min | — |
aloha_multitask_scripted |
3000 | ~25 min | ~9 |
(Full eval results in eval_output/; training checkpoints not committed — ~70 GB total across all runs.)
xarm_push wins (L2=0.216) — 3-DOF pure positional control with no gripper. The simplest task in the lineup. SmolVLA's flow-matching head fits it almost perfectly.
aloha_static_ziploc_slide is the best real-robot result (L2=0.556) — Despite involving precise fine-grained contact with a flexible ziploc bag, this task turns out to be highly repeatable: the robot starts in a consistent configuration, the bag location is predictable, and the motion is a clean slide gesture. SmolVLA handles it better than any other real-robot task.
Real-robot data is not harder than sim — The top-4 real-robot datasets (ziploc 0.556, battery 0.651, cups 0.694, coffee 0.802) all outperform or match the sim ALOHA tasks (insertion 0.716, transfer 0.729). SmolVLA's frozen vision encoder handles real lighting and textures without degradation.
Deformable objects are the hardest real-robot task (L2=1.479) — aloha_static_towel is the worst-performing real-robot run. A towel's shape changes throughout the episode, creating visual states that the model cannot easily map to joint-space actions. This is the clearest embodiment-specific bottleneck found.
Scripted demos: lower train loss, higher open-loop L2 — aloha_insertion_scripted reaches loss 0.040 (3× lower than human 0.118) because scripted trajectories are highly consistent. But the model memorises those exact trajectories and fails to generalise to the slight variations in held-out episodes, giving worse open-loop L2 (0.864 vs 0.716).
xarm_lift fails (L2=1.32) — 84×84 images are too low-resolution to time the gripper open/close action precisely. The position trajectory is fine but the binary gripper signal is consistently mistimed. Confirmed by xarm_push (same 84×84 images, no gripper) being the best result in the entire benchmark.
aloha_multitask pays a ~2× penalty (L2=1.33 vs ~0.72 single-task) — Sharing one set of weights between transfer-cube and peg-insertion produces a representation that's mediocre at both. Even doubling the training budget to 5000 steps doesn't fully recover.
aloha_multitask_scripted fails via normalization mismatch — The scripted transfer task locks certain joints (std[joint0]=0.004), while the scripted insertion task moves those same joints widely (std[joint0]=0.161). Normalising both datasets with the transfer statistics inflates insertion z-scores by 40×, making the loss unreliable (~9–55 vs 0.456 for human multitask). This is a practical lesson: scripted multi-task data requires per-dataset normalisation.
pusht (L2=40 pixels, ~8% of canvas) — The 2-DOF PushT pusher uses pixel coordinates (0–512) instead of radians, so L2 is not comparable to ALOHA/xArm. 40 pixels per step on a 512×512 canvas corresponds to ~8% per-step error — reasonable given the model saw only 206 episodes for 3000 steps. Pusht serves as a reference for the simplest possible continuous control task with visual input.
DATASETS=(aloha_transfer aloha_insertion aloha_multitask
aloha_transfer_scripted aloha_insertion_scripted aloha_multitask_scripted
aloha_static_coffee aloha_static_battery
aloha_static_cups_open aloha_static_towel aloha_static_ziploc_slide
xarm_lift xarm_push xarm_lift_replay xarm_push_replay
pusht)
for d in "${DATASETS[@]}"; do
DATASET=$d $PY train_generic.py
DATASET=$d STEP=3000 $PY eval_generic.py
done
$PY plot_comparison.pyTraining auto-downloads any missing dataset on first run. Data/ is not committed (~4 GB) and checkpoints/ is not committed (~70 GB) — both are listed in .gitignore.
RT-1 vs SmolVLA head-to-head (RT1_repro/)
A second arm of this project reproduces Google's RT-1 (Robotics Transformer, 2022) on the same datasets and the same Mac MPS hardware, using the lucidrains PyTorch port. This isolates the architecture effect: same data, same compute, two different action heads.
| Aspect | SmolVLA | RT-1 (this port) |
|---|---|---|
| Params | 450 M | 243 M (CLIP frozen) |
| Backbone | SmolVLM2 frozen + flow-matching expert | MaxViT-base + Token Learner + 6-layer Transformer |
| Action head | Continuous flow-matching, 10-step denoising | Discrete, 256 bins per dim, cross-entropy |
| History | 1 frame | 6 frames |
| Image size | 512×512 (resize with padding) | 224×224 |
| Training | 3000 steps, BS=4–16 | 2000 steps, BS=4 |
| Dataset | SmolVLA L2 | RT-1 L2 | Δ | Winner |
|---|---|---|---|---|
aloha_transfer |
0.729 | 0.589 | −19% | RT-1 |
aloha_insertion |
0.716 | 0.744 | +4% | SmolVLA (tied) |
aloha_static_battery ★REAL |
0.651 | 0.394 | −40% | RT-1 ★★ |
xarm_push (3-DOF, no gripper) |
0.216 | 0.123 | −43% | RT-1 ★★★ |
xarm_lift (4-DOF, with gripper) |
1.324 | 1.890 | +43% | SmolVLA ★★ |
Score: 3–2 RT-1. But the split is not random — it follows a clean rule:
- RT-1 wins when actions are continuous & stereotyped (transfer, battery, push)
- SmolVLA wins when (a) actions need sub-bin precision near contact (insertion) or (b) the task is hard enough that the pretrained VLM backbone gives real headroom (lift)
1. RT-1 dominates on stereotyped continuous control (up to −43%). On aloha_static_battery, RT-1's per-dim MAE for the left arm (dims 0–7) is 0.006–0.078, while SmolVLA's is 0.025–0.274 — up to 10× lower. On xarm_push (no gripper), RT-1's per-dim MAE is flat 0.055–0.065 vs SmolVLA's 0.064–0.159. Discrete action tokens (256 bins) recover the highly stereotyped joint trajectories of real demos with much less precision loss than continuous flow-matching. The action space "rounds to the nearest bin" exactly when the demonstrator's hand was tremor-stable.
2. Discrete actions fail on hard tasks (+43% worse on xarm_lift). This is the surprising flip: on xarm_lift, RT-1's per-dim MAE is 0.70 / 0.83 / 0.81 / 0.80 vs SmolVLA's 0.53 / 0.58 / 0.59 / 0.56 — RT-1 is uniformly 30–40% worse on every dim, not just the gripper. With 2000 steps and no pretrained vision-language backbone, RT-1 cannot learn xarm_lift's tighter dynamics (84×84 images + binary gripper + larger z-range). SmolVLA's frozen SmolVLM2 backbone provides robust visual representations even on tiny images. The 207M-parameter gap matters most when the task is genuinely hard.
3. SmolVLA wins narrowly on precision-critical contact (+4% on insertion). Peg insertion needs sub-bin angular precision near the contact point. The 256-bin quantization cost shows up here, but only as a 4% tie — noise level.
4. The gripper-binary problem is universal. Both models' worst dim on every ALOHA task is the right-arm gripper (dim 13): RT-1 MAE 0.13–0.24, SmolVLA MAE 0.14–0.27. Gripper is open/close with very few transition frames; both action heads struggle equally. This is a data-side bottleneck (sparse class-imbalanced labels), not architecture-side.
| Regime | Architecture choice |
|---|---|
| Easy continuous task, small data | RT-1 / discrete actions win (-19% to -43%) |
| Precision contact in sim | SmolVLA wins barely (+4%) |
| Hard task with low-res images | SmolVLA / pretrained VLM wins (+43%) |
| Binary signals (gripper) | Both struggle equally — data problem |
Two simultaneous trends are visible:
- Action-head trade-off: discrete bins cost quantization but save inductive bias; continuous flow-matching pays no quantization tax but needs more data to denoise.
- Backbone-capacity trade-off: SmolVLM2's 450M frozen params are not "wasted" — they buy robustness on hard tasks (xarm_lift) even when the action head is identical.
For a Mac-scale reproduction (~50 episodes per task), the right architecture depends on task difficulty:
- Predictable, stereotyped behaviour → use RT-1 (smaller, faster, often better).
- Hard or out-of-distribution behaviour → use SmolVLA-style pretrained backbone for headroom.
Combined model size: 243 M (RT-1) + 450 M (SmolVLA) = 693 M params trained on the same Mac MPS, no NVIDIA GPU.
cd /Users/r/Projects/SmolVLA_ALOHA
pip install robotic-transformer-pytorch tiktoken sentencepiece
# Train + eval on any LeRobot v3 dataset already in dataset_configs.py
DATASET=aloha_static_battery TRAIN_STEPS=2000 $PY RT1_repro/train_rt1.py
DATASET=aloha_static_battery STEP=2000 N_EVAL_EP=8 $PY RT1_repro/eval_rt1.py
# Generate head-to-head plot
$PY RT1_repro/compare_rt1_vs_smolvla.pyThe RT-1 trainer reuses SmolVLA's dataset_configs.py registry and LeRobotDataset helpers — adding new datasets to one project adds them to the other automatically.
SmolVLA_cl — SmolVLA + LIBERO + EEG as a fourth modality. Where this repo studies cross-embodiment generalisation, the cl repo studies whether a brain signal (EEG motor imagery) can be added as a controllable input. Both run on the same hardware (Apple MPS) and share the same SmolVLA architecture; only the embodiments and modalities differ.
Released weights and data:
Twu31/smolvla-cross-embodiment-mps ·
Twu31/rt1-lerobot-mps ·
Twu31/smolvla-libero-eeg ·
Twu31/so101_hand_blue_napkin

