smolvla_tictactoe_a

Post-tuned SmolVLA policy for the SO-101 arm playing tic-tac-toe by picking and placing coloured blocks.
Trained end-to-end through the strands-robots training abstraction on the public HashtagRobotics/tic-tac-toe-so101-block-a-clean-v1 LeRobot v3 dataset.

This model is the "block-A" post-tune — first of a planned series. Vision + language + 6-DoF joint action, 30 Hz control.


TL;DR

Base model lerobot/smolvla_base
Dataset HashtagRobotics/tic-tac-toe-so101-block-a-clean-v1 (195 episodes, ~144k frames @ 30fps)
Robot SO-101 follower (6-DoF, shoulder_pan / shoulder_lift / elbow_flex / wrist_flex / wrist_roll / gripper)
Cameras 2× RGB: observation.images.top (mapped → camera1), observation.images.wrist (mapped → camera2)
Training 20,000 steps · batch 8 · full fine-tune · single H100/Thor GPU
Final loss 0.088 (after 2h 20m)
Trainer strands_robots.training.create_trainer("lerobot_local", policy_type="smolvla")
Hardware NVIDIA Thor (aarch64)

Quick load

With strands_robots (recommended)

from strands_robots import create_policy

policy = create_policy(
    "lerobot_local",
    pretrained_name_or_path="cagataydev/smolvla_tictactoe_a",
    policy_type="smolvla",
    device="cuda",
    trust_remote_code=True,
)
# → policy.get_actions(observation, instruction=...) returns 6-DoF SO-101 actions.

With raw LeRobot

from lerobot.policies.smolvla.modeling_smolvla import SmolVLAPolicy

policy = SmolVLAPolicy.from_pretrained("cagataydev/smolvla_tictactoe_a")

In strands_robots simulation (MuJoCo)

from strands_robots import Robot

sim = Robot("so101", mesh=False)
sim.add_camera(name="front", position=[0.50, 0, 0.30], target=[0.22, 0.025, 0.05])
sim.add_camera(name="wrist", position=[0.0, 0.0, 0.05], target=[0.05, 0, 0],
               parent_body="so101/gripper", fov=70.0)

sim.run_policy(
    robot_name="so101",
    policy_provider="lerobot_local",
    policy_config={
        "pretrained_name_or_path": "cagataydev/smolvla_tictactoe_a",
        "policy_type": "smolvla",
        "device": "cuda",
    },
    instruction="pick up the red block and place it on the top-left square",
    n_steps=500,
    control_frequency=30,
)

Camera name contract: SmolVLA base expects observation.images.camera1/camera2.
The dataset uses top + wrist → training was done with a rename map (see below). At inference in sim, mount your two cameras and route them through the same rename_map in policy_config, or name your sim cams top and wrist and let the embodiment do the routing.


The task

Post-tuned to control an SO-101 arm to pick up a coloured block and place it on a 3×3 tic-tac-toe grid.
Instructions look like:

  • "pick up the red block and place it on the center square"
  • "place a blue block on the top-right"
  • "put your piece on the middle-left square"

The A variant is trained on the cleaned "block-A" split (single-block-type demonstrations, 195 episodes). Future variants (_b, _multiblock, ...) will follow.


Training recipe

Training was driven entirely through the strands_robots.training DX, not raw lerobot.scripts.lerobot_train — this was an end-to-end test of that abstraction on a real public HF dataset.

Full config

from strands_robots.training import TrainSpec, create_trainer

trainer = create_trainer("lerobot_local", policy_type="smolvla")

spec = TrainSpec(
    dataset_repo_id="HashtagRobotics/tic-tac-toe-so101-block-a-clean-v1",
    base_model="lerobot/smolvla_base",
    output_dir="./checkpoints",
    steps=20_000,
    global_batch_size=8,
    method="full",              # full fine-tune (not LoRA, not expert-only)
    save_freq=1000,
    num_gpus=1,
    seed=42,
    streaming=False,            # full local download
    extra={
        "policy_type": "smolvla",
        "job_name": "smolvla_tictactoe_a",
        "rename_map": {
            "observation.images.top": "observation.images.camera1",
            "observation.images.wrist": "observation.images.camera2",
        },
    },
)

result = trainer.train(spec)
# → status=success, checkpoint at result.checkpoint_dir

Hyperparameters

Knob Value
Steps 20,000
Global batch size 8
Learning rate LeRobot default cosine schedule (peak ≈ 1.5e-4, end ≈ 2.5e-6)
Optimizer AdamW
Method full (all params trainable)
Precision bf16 mixed
Chunk size 50 (SmolVLA default)
GPUs 1 × NVIDIA Thor
Seed 42
Data full download, LeRobot v3 format
Save frequency every 1000 steps
Wall-clock 2h 20m (≈2.4 steps/s average)

Loss curve (from training log)

Step Samples Epoch Loss Grad-norm LR
200 2K 0.01 0.352 6.19 1.5e-05
400 3K 0.02 0.254 4.73 4.5e-05
600 5K 0.03 0.273 4.82 7.5e-05
5,000 ~40K 0.26 0.147 ~2.1 ~1.4e-04
10,000 ~80K 0.52 0.109 ~1.8 ~9.8e-05
14,000 ~112K 0.72 0.097 ~1.7 ~4.9e-05
15,000 120K 0.78 0.095 1.71 1.7e-05
18,000 144K 1.00 0.092 1.57 5.1e-06
19,000 152K 1.05 0.087 1.53 3.2e-06
20,000 160K 1.11 0.088 1.61 2.5e-06

Loss dropped from 0.35 → 0.088 over 20K steps. First epoch reached at ~step 18,000 (1× full traversal of 144K frames).

Checkpoints

Automatic checkpoints saved every 1000 steps to checkpoints/{step}/pretrained_model. This upload is the step 20,000 / final checkpoint.


Dataset details

Source HashtagRobotics/tic-tac-toe-so101-block-a-clean-v1
Format LeRobot v3 (parquet)
Episodes 195
Frames ~144,000
FPS 30
Robot type so101
State/action dim 6 (joint positions)
Camera resolution 480×640

Feature routing (rename_map):

observation.images.top    → observation.images.camera1
observation.images.wrist  → observation.images.camera2

camera3 is not populated — SmolVLA-base tolerates missing 3rd cam when only 2 of 3 slots are provided, but you should apply the same rename at inference time.


Files in this repo

File Purpose
config.json Full SmolVLA policy config (frozen at training time)
model.safetensors 906 MB, all trainable params (VLM backbone + action expert)
policy_preprocessor.json + .safetensors Input normalizers (state/image scaling)
policy_postprocessor.json + .safetensors Output unnormalizer (action denormalization)
train_config.json Full training config for reproducibility (steps, batch, lr schedule, seed, …)

policy_preprocessor.json and policy_postprocessor.json are required for correct action denormalization — do not omit them when loading.


Reproducibility

To reproduce this exact model on your hardware:

git clone https://github.com/strands-labs/robots
cd robots && pip install -e '.[lerobot,smolvla]'

Then run the shipped training script:

# train_smolvla_tictactoe.py — same script that trained this checkpoint
from strands_robots.training import TrainSpec, create_trainer

trainer = create_trainer("lerobot_local", policy_type="smolvla")
spec = TrainSpec(
    dataset_repo_id="HashtagRobotics/tic-tac-toe-so101-block-a-clean-v1",
    base_model="lerobot/smolvla_base",
    output_dir="./checkpoints",
    steps=20_000,
    global_batch_size=8,
    method="full",
    save_freq=1000,
    seed=42,
    extra={
        "policy_type": "smolvla",
        "rename_map": {
            "observation.images.top":   "observation.images.camera1",
            "observation.images.wrist": "observation.images.camera2",
        },
    },
)
trainer.train(spec)

Wall-clock on NVIDIA Thor: 2h 20m for 20K steps.


Evaluation

Post-training in-domain training loss: 0.088.

Real-world evaluation on physical SO-101 hardware and MuJoCo sim (using the strands_robots sim pipeline to recreate the tic-tac-toe scene) is in progress — will publish success rates + video rollouts as a follow-up here + on Telegram/GitHub.

Planned eval metrics:

  • Task success rate: block correctly placed on target grid square (10 trials × 9 squares × 3 colours).
  • Sim-to-real gap: same policy in MuJoCo scene reconstruction vs. real SO-101.
  • Instruction following: text-conditioned generalisation to unseen instructions (colour × position combos).

Known limitations

  1. Single dataset variant — trained only on the "block-A clean" split. May not generalise to different block colours/shapes without further tuning.
  2. Two cameras only — no wrist-only or third-person-only ablation yet.
  3. 6-DoF joint control — inherits the SO-101 kinematic constraints (no end-effector cartesian control).
  4. Rename map is required at inference — if you name cameras differently, you must apply the same routing.
  5. No LoRA / expert-only variant yet — full fine-tune only (906 MB). LoRA variants are planned to drop below 100 MB.

Citation & lineage

Please cite the base model + dataset if you use this checkpoint.


Contact

Downloads last month
5
Safetensors
Model size
0.5B params
Tensor type
F32
·
BF16
·
Video Preview
loading

Model tree for cagataydev/smolvla-tictactoe

Finetuned
(7241)
this model

Dataset used to train cagataydev/smolvla-tictactoe

Evaluation results

  • Final Training Loss on tic-tac-toe-so101-block-a-clean-v1
    self-reported
    0.088
  • Loss After Padding Removal on tic-tac-toe-so101-block-a-clean-v1
    self-reported
    0.085