Views
No views yet
strands-robots training abstraction (create_trainer("lerobot_local", policy_type="smolvla")).When to use this checkpoint: OOD scenes where the base VLM's community-pretrained features don't cover the visual domain (tic-tac-toe board, custom lighting, specific object appearance). For in-distribution manipulation, the frozen-vision variant is faster and comparable.
| Metric | Value |
|---|---|
| Base model | lerobot/smolvla_base |
| Steps | 60,000 |
| Trainable params | 393M / 450M (vision + expert) |
| Final loss | 0.078 |
| Wall time | ~10h 40m on 1× GPU |
| Throughput | ~1.6 step/s, ~13 samples/s |
| Peak GPU mem | 8.85 GB |
| Samples seen | 480K (~3.32 epochs) |
| Final LR | 2.5e-6 (cosine schedule) |
| Dataset | 195 episodes, ~144K frames @ 30 Hz, SO-101 (6-DoF) |
| Cameras | observation.images.top, observation.images.wrist |
| Framework | strands-robots training.create_trainer("lerobot_local") |
| Step | Loss |
|---|---|
| 10K | ~0.15 |
| 20K | ~0.11 |
| 30K | ~0.09 |
| 40K | ~0.08 |
| 50K | ~0.075 |
| 60K | 0.078 |
freeze_vision_encoder=True + train_expert_only=True, which gives ~100M trainable (action-expert only). To train the full stack we override both to False:1extra={
2 "policy_type": "smolvla",
3 "policy.freeze_vision_encoder": False, # MUST be Python bool, not "false" string
4 "policy.train_expert_only": False,
5}num_learnable_params:99,880,992 (~100M) → expert-only, vision frozen392,904,096 (~393M) → vision-unfrozen ✓450,046,176 (~450M) → total (frozen + trainable)strands_robots.training abstraction, not raw lerobot.scripts.lerobot_train — is reproduced below.1# 1. Env
2git clone https://github.com/strands-labs/robots.git
3cd robots
4pip install -e ".[lerobot,sim-mujoco]"
5
6# 2. Kick training (single GPU, 60K steps, vision unfrozen)
7python train_smolvla_tictactoe.py \
8 --steps 60000 --batch 8 \
9 --repo-id HashtagRobotics/tic-tac-toe-so101-block-a-clean-v1 \
10 --base-model lerobot/smolvla_base \
11 --out ./checkpoints \
12 --streamingtrain_smolvla_tictactoe.py (full source)1"""Train SmolVLA on HashtagRobotics/tic-tac-toe-so101-block-a-clean-v1 via
2strands_robots training abstraction (NOT raw lerobot.scripts.lerobot_train).
3
4Tests the strands_robots.training.create_trainer("lerobot_local", policy_type="smolvla")
5DX end-to-end on a real public HF dataset.
6
7Dataset: 195 episodes, ~144k frames @ 30fps, SO-101 (6-DoF joint state/action),
8 two cameras: observation.images.top, observation.images.wrist.
9Base: lerobot/smolvla_base.
10"""
11from __future__ import annotations
12
13import argparse
14import sys
15import time
16from pathlib import Path
17
18from strands_robots.training import TrainSpec, create_trainer
19
20
21def main() -> int:
22 ap = argparse.ArgumentParser()
23 ap.add_argument("--smoke", action="store_true", help="Tiny run (200 steps, batch=4)")
24 ap.add_argument("--steps", type=int, default=20000)
25 ap.add_argument("--batch", type=int, default=8)
26 ap.add_argument("--lr", type=float, default=None)
27 ap.add_argument("--method", default="full", choices=["full", "lora", "expert_only"])
28 ap.add_argument("--out", default="./checkpoints")
29 ap.add_argument("--dataset-root", default=None)
30 ap.add_argument("--repo-id", default="HashtagRobotics/tic-tac-toe-so101-block-a-clean-v1")
31 ap.add_argument("--base-model", default="lerobot/smolvla_base")
32 ap.add_argument("--streaming", action="store_true",
33 help="Stream shards from HF instead of full download.")
34 ap.add_argument("--save-freq", type=int, default=1000)
35 ap.add_argument("--val-episodes", type=int, default=None)
36 ap.add_argument("--seed", type=int, default=42)
37 ap.add_argument("--validate-only", action="store_true")
38 args = ap.parse_args()
39
40 if args.smoke:
41 args.steps, args.batch, args.save_freq = 200, 4, 100
42
43 Path(args.out).mkdir(parents=True, exist_ok=True)
44
45 # === strands_robots training abstraction ===
46 trainer = create_trainer("lerobot_local", policy_type="smolvla")
47 print(f"[strands_robots] trainer={type(trainer).__name__} provider={trainer.provider_name}")
48 print(f"[strands_robots] hardware_floor: {trainer.hardware_floor}")
49
50 spec = TrainSpec(
51 dataset_repo_id=args.repo_id,
52 dataset_root=args.dataset_root or "",
53 base_model=args.base_model,
54 output_dir=args.out,
55 steps=args.steps,
56 global_batch_size=args.batch,
57 learning_rate=args.lr,
58 method=args.method,
59 save_freq=args.save_freq,
60 val_episodes=args.val_episodes,
61 num_gpus=1,
62 seed=args.seed,
63 streaming=args.streaming,
64 extra={
65 "policy_type": "smolvla",
66 "job_name": "smolvla-tictactoe-60k-vision-unfrozen",
67 # smolvla_base expects observation.images.camera{1,2,3} — dataset
68 # has top/wrist, so rename them.
69 "rename_map": {
70 "observation.images.top": "observation.images.camera1",
71 "observation.images.wrist": "observation.images.camera2",
72 },
73 # === VISION UNFROZEN ===
74 # Defaults: freeze_vision_encoder=True + train_expert_only=True → ~100M trainable.
75 # Overriding both to Python False → ~393M trainable.
76 # WARNING: in-process passthrough uses setattr(), so values MUST be Python
77 # booleans, not strings. String "false" is truthy → freeze stays on.
78 "policy.freeze_vision_encoder": False,
79 "policy.train_expert_only": False,
80 },
81 )
82
83 # Preflight
84 print("\n[validate] running preflight...")
85 problems = trainer.validate(spec)
86 if problems:
87 print("[validate] PROBLEMS:")
88 for p in problems:
89 print(" •", p)
90 if not args.validate_only:
91 return 1
92 else:
93 print("[validate] ✅ no problems.")
94
95 # Show argv-parity CLI (what the abstraction would call under the hood)
96 try:
97 cmd = trainer.build_command(spec)
98 print("\n[argv-parity] equivalent lerobot CLI:")
99 print(" " + " \\\n ".join(cmd))
100 except Exception as e:
101 print(f"[argv-parity] build_command failed: {e}")
102
103 if args.validate_only:
104 return 0
105
106 # Prepare + train
107 print("\n[prepare]")
108 trainer.prepare(spec)
109
110 print(f"\n[train] launching in-process — steps={args.steps} batch={args.batch}")
111 t0 = time.time()
112 try:
113 result = trainer.train(spec)
114 except KeyboardInterrupt:
115 print("\n[train] interrupted by user")
116 return 130
117 dt = time.time() - t0
118
119 print(f"\n[train] finished in {dt/60:.1f} min")
120 print(f" status: {result.status}")
121 print(f" job_id: {result.job_id}")
122 print(f" checkpoint_dir: {result.checkpoint_dir}")
123 print(f" exported_model: {result.exported_model}")
124 for k, v in (result.metrics or {}).items():
125 print(f" metric[{k}]: {v}")
126
127 if result.status == "success" and result.checkpoint_dir:
128 exported = trainer.export(spec, result.checkpoint_dir)
129 print(f"\n[export] exported model → {exported}")
130 print(f"[export] load with:")
131 print(f" create_policy('lerobot_local', pretrained_name_or_path='{exported}')")
132
133 return 0 if result.status == "success" else 2
134
135
136if __name__ == "__main__":
137 sys.exit(main())strands_robots.create_policy (recommended)1from strands_robots import Robot, create_policy
2
3# Load this fine-tuned checkpoint
4policy = create_policy(
5 "lerobot_local",
6 pretrained_name_or_path="cagataydev/smolvla_tictactoe_vision_unfrozen",
7 policy_type="smolvla",
8 device="cuda",
9)
10
11# Attach to an SO-101 in MuJoCo
12sim = Robot("so101", mesh=False)
13sim.add_camera(name="top", position=[0.5, 0, 0.4], target=[0.2, 0, 0.05])
14sim.add_camera(name="wrist", parent_body="so101/gripper",
15 position=[0.0, 0.0, 0.05], target=[0.05, 0.0, 0.0], fov=70.0)
16
17sim.run_policy(
18 robot_name="so101",
19 policy=policy,
20 instruction="place the block on the tic-tac-toe board",
21 n_steps=300,
22 control_frequency=30,
23 # Feed cameras with the same rename map used at training time
24 camera_key_map={
25 "top": "observation.images.camera1",
26 "wrist": "observation.images.camera2",
27 },
28 video={"path": "rollout.mp4", "camera": "top", "fps": 30},
29)lerobot1from lerobot.policies.factory import make_policy
2from lerobot.configs.policies import PreTrainedConfig
3
4cfg = PreTrainedConfig.from_pretrained("cagataydev/smolvla_tictactoe_vision_unfrozen")
5policy = make_policy(cfg=cfg, ds_meta=None) # or your dataset's meta
6policy.eval().to("cuda")
7
8# obs is a dict:
9# observation.state: (1, 6) float32 — SO-101 joint positions
10# observation.images.camera1: (1, 3, H, W) float32 in [0,1] — top cam
11# observation.images.camera2: (1, 3, H, W) float32 in [0,1] — wrist cam
12# language: "place the block on the tic-tac-toe board"
13
14action = policy.select_action(obs, task=language)1from strands_robots import Robot, create_policy
2
3# 1. Connect real SO-101 (via feetech STS3215 bus)
4robot = Robot("so101", real=True, port="/dev/ttyACM0")
5robot.calibrate()
6
7# 2. Load policy
8policy = create_policy(
9 "lerobot_local",
10 pretrained_name_or_path="cagataydev/smolvla_tictactoe_vision_unfrozen",
11 policy_type="smolvla",
12 device="cuda",
13)
14
15# 3. Attach cameras (Intel RealSense / USB webcams)
16robot.add_camera("top", device=0) # top-mounted USB cam
17robot.add_camera("wrist", device=1) # wrist-mounted USB cam
18
19# 4. Roll out
20robot.run_policy(
21 policy=policy,
22 instruction="place the block on the tic-tac-toe board",
23 control_frequency=30,
24 duration=30.0,
25 camera_key_map={
26 "top": "observation.images.camera1",
27 "wrist": "observation.images.camera2",
28 },
29)observation.images.camera{1,2,3}. The tic-tac-toe dataset ships them as top / wrist. Both training and inference MUST apply the same rename map:| Sim/dataset name | Model input key |
|---|---|
observation.images.top | observation.images.camera1 |
observation.images.wrist | observation.images.camera2 |
KeyError: 'observation.images.camera1' at model.forward.lerobot (via strands_robots[lerobot] extra)strands_robots >= 0.6.0torch >= 2.6.0transformers >= 4.50peft (only needed if --method lora)--method lora in the same script.1@misc{cagatay2026smolvla_tictactoe_vision_unfrozen,
2 title = {SmolVLA Fine-tune on TicTacToe SO-101 (Vision Unfrozen)},
3 author = {cagataydev},
4 year = {2026},
5 url = {https://huggingface.co/cagataydev/smolvla_tictactoe_vision_unfrozen}
6}
7
8@article{smolvla2025,
9 title = {SmolVLA: A Vision-Language-Action Model for Affordable and Efficient Robotics},
10 author = {Hugging Face LeRobot Team},
11 year = {2025},
12 url = {https://arxiv.org/abs/2506.01844}
13}training.create_trainer("lerobot_local") on Thor (NVIDIA aarch64). Full training script above is self-contained and reproducible.