亚马逊Score B (57)

Record, train, and deploy from one place with Strands Agents, LeRobot, and Hugging Face Storage Buckets

16 小时前2 viewsSource: HuggingFace Blog

Record, train, and deploy from one place with Strands Agents, LeRobot, and Hugging Face Storage Buckets

Enterprise Article
Published August 13, 2026

A walkthrough of the streaming data loop in Strands Robots, one agent loop that records robot demonstrations, trains on them by reading straight from the Hub, and deploys the policy back to hardware, with the dataset in the same on-disk LeRobot format the whole way through.

You have an agent that can already record a demonstration and push it to the Hugging Face Hub. Now you want to run that loop continuously: collect episodes through the day, train a policy on the growing dataset, deploy it, and pull the next batch back to improve it. Run that loop once and every piece works. Run it every day and you start paying for the same byte transfers over and over. The recordings you upload keep growing, each training run copies the whole dataset to the GPUs before it starts, and every new checkpoint ships out while the next batch of recordings comes back.

The first post in this series introduced Strands Robots, an open source SDK from AWS (Apache 2.0) that exposes robot abstractions, simulation, and the LeRobot stack as AgentTools you compose into a single Strands agent. It covered the Robot() factory, recording a demonstration in simulation, running a policy, and deploying the same agent code to a physical SO-101. That factory resolves a name against a registry of arms, humanoids, mobile bases, and hands, so the SO-100 used throughout this post is one of many supported embodiments. The robot catalog lists every robot the factory knows about. LeRobot's dataset format is already used by over 90,000 datasets and models on the Hub from more than 8,000 publishers (LeRobot Project Pulse). A Strands Robots recording is one more of them, so anything built to read LeRobot data can read it without conversion. If you are new to Strands Robots, start there; this post assumes that setup.

That post followed the agent loop in one direction, from a Hub dataset to a physical robot. This one follows the data the other way, from the first recorded frame back to the deployed policy, over Hugging Face Storage Buckets - a mutable, non-versioned, Xet-backed object-storage repository type announced in March 2026. A bucket sits beside your dataset repositories in the same hf:// namespace and uses the hf CLI you already have, so it becomes the working layer that holds your data between the day you record it and the day you train on it.

Someone has to decide which episodes to keep, when the scene has drifted far enough to re-record, whether today's batch is enough to train on, and which checkpoint replaces the one on the arm. Each of those decisions comes up dozens of times over a collection campaign, and each one needs a look at what came back before the next command goes out. That is the work an agent is for. This post walks you through the data loop inside a single agent: record a demonstration into a Storage Bucket, store it so that each sync uploads only the bytes that changed, train by streaming the dataset straight from the Hub instead of downloading it, and deploy the checkpoint back to hardware with one keyword argument change. The runnable companion to this post lives at examples/notebooks/05_streaming_data_loop.ipynb.

What you'll build

Where the first post recorded a dataset and pushed it to the Hub, the agent you build here records a LeRobotDataset from a natural-language prompt, syncs it into a Storage Bucket, and streams that same dataset back frame by frame, decoding camera video on the fly, with no local copy. You read it back in the same process that wrote it: the same Strands Robots Robot() that recorded the dataset streams it. Your trained checkpoint then deploys to that same Robot() with one keyword argument change, and the demonstrations it records on hardware return to the same bucket.

The four stages of the Strands Robots data loop, cycling one at a time: record writes a LeRobotDataset, store syncs it to a Hugging Face Bucket, train streams it back to the GPU, and deploy runs the checkpoint on hardware, with each stage lighting the arrow that feeds it around one shared Robot()

Figure 1. The four stages share one backend. Robot("so100") records a LeRobotDataset through the shared DatasetRecorder; sync_dataset_to_bucket(...) syncs it into a Storage Bucket; stream_dataset(...) reads it back over the Hub with no full download; and the trained checkpoint deploys to the same Robot with mode="real". The on-disk format stays exactly as LeRobot wrote it.

Because one Robot() both records a dataset and reads it back, collecting data and training on it are two methods on one object over one backend. The agent decides to run an episode and invokes one tool; the rollout then proceeds at the robot's control frequency until the episode ends, with the trained policy producing every action. The whole loop, in a handful of lines:

from strands import Agent
from strands_robots import Robot

sim = Robot("so100")                 
agent = Agent(tools=[sim])


agent("Record a pick-the-cube demo and sync it to my-org/robot-fave.")


for batch in sim.stream_dataset("my-org/robot-fave/cube_pick", repo_type="bucket").dataloader(batch_size=64):
    ...

What follows is what's actually happening inside that loop, step by step.

Prerequisites

Minimal (default simulation path)

  • Python 3.12+, on Linux or macOS (Apple Silicon supported for the MuJoCo backend).
  • A Strands-compatible model provider for the agent's reasoning. Amazon Bedrock with AWS credentials, the Anthropic API, OpenAI, or Ollama running locally.
  • Strands Robots with the dataset extras: uv pip install -U "strands-robots[sim-mujoco,lerobot]>=0.5.1". The lerobot extra pulls in LeRobot (>=0.6.1), datasets, av, and torchcodec, so recording and video decode both work without further setup. Refer to installation guide.

That's it. Every stage in this post runs on a laptop with these three. What runs is the loop, not a working policy: the default path uses a mock policy, which records a valid dataset but not a useful one.

Advanced (buckets, hardware, real policies)

  • A Hugging Face account and a token with write permission, plus the hf CLI for creating buckets and syncing datasets: pip install -U "huggingface-hub>=1.6.0,<2.0.0", then hf auth login.
  • For the hardware path: an SO-101 follower and leader pair, or any other LeRobot-supported robot, with calibration files under ~/.cache/huggingface/lerobot/calibration/.
  • For local vision-language-action (VLA) inference: an NVIDIA GPU. For training at scale, a GPU cluster reading from the Hub.
  • To run the training step: uv pip install "lerobot[training]". Recording and streaming do not need it. If you skip it, trainer.train() returns an error result rather than a checkpoint. The troubleshooting guide names that error and the install that fixes it.

Step 1 - Record a demonstration into a bucket

You record new episodes through the day, each a continuous run of camera frames and joint state-action telemetry. LeRobot writes that as a small set of large files that grow as you record. Push them into a versioned dataset repository and every append becomes a commit, and every revision is retained. Collection wants the reverse: somewhere to write bytes and overwrite them in place. That is a Storage Bucket, which lives inside your Hugging Face workspace and uses the permissions you already have. There are no identity and access management (IAM) roles to configure, no cross-origin resource sharing (CORS) rules, and no upload service to maintain.

Your agent records a LeRobotDataset in the same format LeRobot writes on hardware. Record the episode, then sync the finished dataset into a bucket. The prompt asks for the mock policy, a stand-in that produces joint actions without a trained model, so you can run the whole loop before you have a checkpoint to run:

from strands import Agent
from strands_robots import Robot, sync_dataset_to_bucket

sim = Robot("so100")                 
agent = Agent(tools=[sim])

agent(
    "Create a world with the so100 robot, add a red cube and a front camera, "
    "start recording (repo_id='local/cube_pick', root='/tmp/cube_pick', fps=30, "
    "overwrite=True, task='pick up the red cube'), run the mock policy for "
    "60 steps, then stop recording."
)

sync_dataset_to_bucket("/tmp/cube_pick", "my-org/robot-fave")

The sync writes to hf://buckets/{bucket}/{run_id}, where run_id defaults to the dataset directory name. The streaming read in Step 3 names the run too: the first two segments of the id are the bucket, and everything after them is the path inside it.

sync_dataset_to_bucket(root, bucket, run_id=...) validates the dataset and syncs it through the hf CLI, decoupled from the recording lifecycle. The same capability is on DatasetRecorder.sync_to_bucket(bucket, run_id=...) if you drive an open recorder directly, and stop_recording(bucket=...) syncs at the moment you stop an active recording. The bucket is the working layer you write to through the day; for the versioned, published artifact you still call push_to_hub(). Both hold the same format.

The episode is structurally complete, but the actions are placeholders, so it is not training data you would want. Swap in a real policy with create_policy("<hf_repo>") for actual grasping; the prompt, the format, and the bucket sync stay identical.

Recording on hardware

To record on a physical SO-101, LeRobot's record CLI handles the leader-follower bring-up:

lerobot-record \
  --robot.type=so101_follower --robot.id=my_follower \
  --teleop.type=so101_leader  --teleop.id=my_leader \
  --dataset.repo_id=my_user/cube_picking \
  --dataset.single_task='Pick up the red cube'

The dataset lands on disk in the same format as the simulation recording, so the same sync call takes it to a bucket: sync_dataset_to_bucket("./recordings", "my-org/robot-fave", run_id="run-021") (or the hf sync ./recordings hf://buckets/my-org/robot-fave/run-021 CLI it wraps). Collection runs append into one place, and your published repositories only get the versions you choose to publish.

Step 2 - Store with byte-level deduplication

Now that a dataset is in the bucket, the question is what the next sync costs you. Point two fixed cameras at an arm clearing the same table for eight hours and most of what you record is pixels you already have: the same lighting, the same chassis, the same background, across thousands of episodes. On a versioned repository it gets worse, because changing one frame in a multi-gigabyte video shard re-uploads the whole file.

Buckets are backed by Xet, which deduplicates your uploads at the byte level using content-defined chunking. Chunk boundaries follow the content, so inserting a few bytes changes only the chunk it lands in instead of shifting every boundary after it. In Hugging Face's own measurements (HF Storage), content-defined chunking reduces data transferred per upload by about four times across the Hub, and on Enterprise plans billing is on the deduplicated footprint. Their bucket benchmarks show what that looks like on a single file. Starting from a 500 MB upload, changing 1% of the bytes and re-uploading moved 5.5 MB, changing 5% moved 27.5 MB, and changing 10% moved 55 MB. Without chunk-level deduplication, overwriting an object means sending all of its bytes again, whether or not they changed.

How much that saves you depends on the file layout, and the Strands Robots recorder uses LeRobot's. Episodes go into Parquet shards (data/chunk-000/file-000.parquet) and per-camera MP4 shards (videos/observation.images.front/chunk-000/file-000.mp4), rolling to a new file only when the current one fills, at LeRobot's defaults of 100 MB for data Parquet and 200 MB for video MP4. So a sync after a day of recording uploads the new trailing shards plus the one partially-filled shard that grew, rather than the whole dataset. Sync the same bucket again tomorrow and Xet handles the deduplication.

fig2_xet_dedup

Figure 2. A sync uploads only what changed. The first sync of a fresh dataset uploads every chunk; after recording more episodes, Xet's content-defined chunking means the next sync uploads only the new chunks and skips the ones already stored.

Step 3 - Train by streaming from the Hub

To train, you point GPUs at your dataset. Download it first and those GPUs sit idle until hundreds of gigabytes finish copying. Streaming straight from the Hub works here because of the shard layout from Step 2: a batch becomes a few byte-range reads over large shards rather than thousands of small fetches. LeRobot's StreamingLeRobotDataset turns that into a drop-in torch iterable, and Strands Robots exposes it through stream_dataset():

Two paths from a Hugging Face Bucket to a training GPU, shown one at a time: the download path moves the dataset to local disk and then to a GPU that waits, while the stream path sends it straight to a GPU that is already busy, with 100 KB on local disk

Figure 3. Stream, don't download. The download path copies the whole dataset to local disk first, so the GPU waits; stream_dataset() reads batches straight from the bucket with nothing on local disk, so the GPU trains from the first batch.

reader = sim.stream_dataset("my-org/robot-fave/cube_pick", repo_type="bucket",
    shuffle=False, max_num_shards=1, buffer_size=1,  
)

print(reader.num_episodes, reader.num_frames, reader.fps)
for frame in reader:
    frame["observation.images.front"]   
    frame["observation.state"]           
    frame["action"]
    break

Nothing lands on local disk except the small meta/ folder of schema, statistics, and episode index. Camera frames are decoded from the remote MP4 shards as you iterate; state and action come from the Parquet shards. That loop reads one frame at a time, which suits inspecting an episode. To train, pass the reader to a DataLoader and iterate batches instead. The streaming dataset shuffles internally through a bounded reservoir buffer, so video decoding parallelizes across worker processes, and the training step itself is the ordinary PyTorch one:


for batch in reader.dataloader(batch_size=64, num_workers=4):
    loss, _ = policy(batch)   
    loss.backward()

If you would rather not write the loop at all, LeRobot's own trainer reads through the same engine, so the dataset your agent collected trains without a line of new code. It takes a bucket through the same keyword argument the in-process reader uses:

lerobot-train --policy.type=act \
  --dataset.repo_id=my-org/robot-

Read the full original article:

HuggingFace Blog