{ "cells": [ { "cell_type": "markdown", "id": "7fb27b941602401d91542211134fc71a", "metadata": {}, "source": [ "# The Streaming Data Loop\n", "\n", "The full physical-AI data loop in one notebook: **record** a dataset into a\n", "Hugging Face Storage Bucket, **stream** it back with no download, **train** a\n", "policy on the streamed data, and **load** the checkpoint ready to deploy.\n", "\n", "This is the notebook version of\n", "[`examples/06_agent_collect_and_stream.py`](https://github.com/strands-labs/robots/blob/main/examples/06_agent_collect_and_stream.py), the agent-driven data-loop example.\n", "\n", "**Requirements:** the bucket path needs `strands-robots >= 0.5.1`, which is the\n", "first release whose `[lerobot]` extra floors LeRobot at the `>= 0.6.1` that\n", "serves `stream_dataset(..., repo_type=\"bucket\")`.\n", "\n", "```bash\n", "pip install -U \"strands-robots[sim-mujoco,lerobot]>=0.5.1\"\n", "```\n", "\n", "Keep the `-U` and the version floor on any install line you adapt. Extras alone\n", "do not make a requirement unsatisfied, so a plain install into an environment\n", "that already has an older release reports `Requirement already satisfied` and\n", "upgrades nothing. On 0.4.1 the bucket read below then raises `TypeError: open()\n", "got an unexpected keyword argument 'repo_type'`, naming the keyword rather than\n", "the stale install behind it.\n", "\n", "Bucket steps additionally need `hf auth login`. Step 5 needs\n", "`lerobot[training]` on CPU as well as GPU - the trainer imports `accelerate`\n", "either way. The sim-only path (record + stream from a local root) runs on any\n", "laptop, no GPU, no credentials.\n", "\n", "The optional Step 7 (Isaac backend swap) additionally needs an RTX GPU +\n", "Isaac Sim 6.0+ and the `sim-isaac` extra; it self-skips otherwise." ] }, { "cell_type": "code", "execution_count": null, "id": "acae54e37e7d407bbb7b55eff062a284", "metadata": {}, "outputs": [], "source": [ "import os\n", "import shutil\n", "import sys\n", "\n", "# macOS uses \"cgl\" for offscreen GL; Linux headless uses \"egl\".\n", "os.environ.setdefault(\"MUJOCO_GL\", \"cgl\" if sys.platform == \"darwin\" else \"egl\")\n", "os.environ.setdefault(\"STRANDS_TRUST_REMOTE_CODE\", \"1\")\n", "\n", "ROOT = \"/tmp/nb5_dataset\"\n", "OUT = \"/tmp/nb5_ft\"\n", "BUCKET = None # set to \"your-org/your-bucket\" to enable the bucket path\n", "RUN_ID = \"nb5_demo\" # folder inside the bucket; one run per collection session\n", "\n", "shutil.rmtree(ROOT, ignore_errors=True)\n", "shutil.rmtree(OUT, ignore_errors=True)" ] }, { "cell_type": "markdown", "id": "9a63283cbaf04dbcab1f6479b197f3a8", "metadata": {}, "source": [ "## 1. Record a demonstration\n", "\n", "`Robot(\"so100\")` builds a MuJoCo simulation. We add a camera (so the dataset\n", "has video), run the Mock policy for 60 steps (structurally complete data), and\n", "stop recording. The result is a LeRobotDataset on disk." ] }, { "cell_type": "code", "execution_count": null, "id": "8dd0d8092fe74a7c96281538738b07e2", "metadata": {}, "outputs": [], "source": [ "from strands_robots import MockPolicy, Robot, create_policy\n", "from strands_robots.training import TrainSpec, create_trainer\n", "\n", "sim = Robot(\"so100\", mesh=False)\n", "sim.add_camera(name=\"front\", position=[0.5, 0.0, 0.4], target=[0.2, 0.0, 0.05])\n", "sim.start_recording(\n", " repo_id=\"local/nb5_demo\",\n", " root=ROOT,\n", " fps=30,\n", " task=\"pick up the red cube\",\n", " cameras=[\"front\"], # record only the declared sensor, not the implicit 'default' view\n", " overwrite=True,\n", ")\n", "# control_frequency must match the recording fps: the recorder writes one frame\n", "# per control step with no decimation, so a 50 Hz default rollout against a 30 fps\n", "# recording is refused rather than written at a distorted timestamp rate.\n", "result = sim.run_policy(\n", " robot_name=\"so100\",\n", " policy_object=MockPolicy(),\n", " instruction=\"pick up the red cube\",\n", " n_steps=60,\n", " control_frequency=30.0,\n", ")\n", "if result.get(\"status\") != \"success\":\n", " raise RuntimeError(f\"rollout failed: {result.get('content')}\")\n", "\n", "result = sim.stop_recording()\n", "if result.get(\"status\") != \"success\":\n", " raise RuntimeError(f\"recording failed: {result.get('content')}\")\n", "print(\"recorded ->\", ROOT)" ] }, { "cell_type": "markdown", "id": "72eea5119410473aa328ad9291626812", "metadata": {}, "source": [ "## 2. See what the robot recorded\n", "\n", "Render the front camera so you can see the scene the dataset captured." ] }, { "cell_type": "code", "execution_count": null, "id": "8edb47106e1a46a883d545849b8ab81b", "metadata": {}, "outputs": [], "source": [ "from IPython.display import Image, display\n", "\n", "# Render the sim before destroying it.\n", "frame = sim.render(camera_name=\"front\")\n", "for item in frame.get(\"content\", []):\n", " if isinstance(item, dict) and \"image\" in item:\n", " display(Image(data=item[\"image\"][\"source\"][\"bytes\"], width=480))\n", " break\n", "sim.destroy()\n", "print(\"SO-100 arm with front camera - this is what the dataset contains.\")" ] }, { "cell_type": "markdown", "id": "10185d26023b46108eb7d9f57d49d2b3", "metadata": {}, "source": [ "## 3. Sync to a Hugging Face Storage Bucket (optional)\n", "\n", "If you have a bucket (`hf buckets create your-org/name --private` after\n", "`hf auth login`), set `BUCKET` in cell 1. The sync uploads only the bytes that\n", "changed (Xet deduplication), so daily re-syncs are fast.\n", "\n", "**Skip this cell** if running without credentials (the local path still works\n", "for Steps 4 and 5)." ] }, { "cell_type": "code", "execution_count": null, "id": "8763a12b2bbd4a93a75aff182afb95dc", "metadata": {}, "outputs": [], "source": [ "if BUCKET:\n", " from strands_robots.dataset_recorder import DatasetRecorder\n", "\n", " # Reopen the dataset recorded in Step 1 and sync it. sync_to_bucket()\n", " # returns a status dict - check it instead of assuming success.\n", " rec = DatasetRecorder.resume(\"local/nb5_demo\", root=ROOT)\n", " result = rec.sync_to_bucket(BUCKET, run_id=RUN_ID)\n", " if result.get(\"status\") == \"success\":\n", " print(f\"synced to bucket: {result['bucket_uri']}\")\n", " else:\n", " raise RuntimeError(f\"bucket sync failed: {result.get('message')}\")\n", "else:\n", " print(\"BUCKET not set - skipping sync. Local path works for training below.\")" ] }, { "cell_type": "markdown", "id": "7623eae2785240b9bd12b16a66d81610", "metadata": {}, "source": [ "## 4. Stream the dataset back\n", "\n", "`stream_dataset()` is the read counterpart to `start_recording()`. It reads\n", "frames lazily with no full download. If you synced to a bucket (Step 3), pass\n", "`repo_type=\"bucket\"` to stream directly from it (requires LeRobot >= 0.6.1).\n", "\n", "Reading frames in capture order takes three arguments, not one. `shuffle=False`\n", "only fixes the random seed: the reader re-shards the dataset into\n", "`max_num_shards` shards and yields from a random reservoir of `buffer_size`\n", "frames, so `max_num_shards=1, buffer_size=1` are what collapse it to a single\n", "sequential pass. Use all three to inspect or replay an episode; drop all three\n", "to train, where the internal shuffle is what you want and video decoding\n", "parallelizes across worker processes.\n", "\n", "`episodes=[n]` does not filter: LeRobot accepts the argument and never applies\n", "it, so the whole dataset streams back with no error. Filter on\n", "`frame[\"episode_index\"]` while iterating instead.\n", "\n", "`sync_to_bucket` writes each run to its own `run_id` folder inside the bucket\n", "(`hf://buckets/{BUCKET}/{RUN_ID}`), so the streaming repo id is\n", "`f\"{BUCKET}/{RUN_ID}\"`, not `BUCKET` on its own. The bucket namespace is the\n", "first two segments of that id; everything after it is the path within the\n", "bucket. Passing `BUCKET` alone looks for `meta/` at the bucket root and fails\n", "with `FileNotFoundError`." ] }, { "cell_type": "code", "execution_count": null, "id": "7cdc8c89c7104fffa095e18ddfef8986", "metadata": {}, "outputs": [], "source": [ "sim = Robot(\"so100\", mesh=False)\n", "\n", "if BUCKET:\n", " # Read from the same path Step 3 wrote to: bucket namespace + run_id.\n", " bucket_repo_id = f\"{BUCKET}/{RUN_ID}\"\n", " reader = sim.stream_dataset(\n", " bucket_repo_id, repo_type=\"bucket\", shuffle=False, max_num_shards=1, buffer_size=1\n", " )\n", " print(f\"streaming from bucket: hf://buckets/{bucket_repo_id}\")\n", "else:\n", " reader = sim.stream_dataset(\n", " \"local/nb5_demo\", root=ROOT, shuffle=False, max_num_shards=1, buffer_size=1\n", " )\n", " print(f\"streaming from local root: {ROOT}\")\n", "\n", "print(f\"episodes: {reader.num_episodes} | frames: {reader.num_frames} | fps: {reader.fps}\")\n", "\n", "for n, frame in enumerate(reader):\n", " if n == 0:\n", " img = frame.get(\"observation.images.front\")\n", " state = frame.get(\"observation.state\")\n", " print(\n", " f\"frame 0 - image: {tuple(img.shape) if img is not None else None}, state: {tuple(state.shape) if state is not None else None}\"\n", " )\n", " if n >= 4:\n", " break\n", "print(f\"streamed {n + 1} frames - camera decoded on the fly.\")\n", "sim.destroy()" ] }, { "cell_type": "markdown", "id": "b118ea5561624da68c537baed56e602f", "metadata": {}, "source": [ "## 5. Train a policy on the dataset\n", "\n", "`create_trainer(\"lerobot_local\")` returns a Trainer (the peer of\n", "`create_policy()`). A `TrainSpec` describes the run. Here we train ACT for 2\n", "steps on CPU (fast for the demo). On a GPU with 500 steps this takes about 133\n", "seconds on an NVIDIA L4 (`g6.4xlarge`).\n", "\n", "This step needs `lerobot[training]` whichever device you are on: the trainer\n", "imports `accelerate` before it looks at the device. The cell checks\n", "`result.status` and re-raises `result.message`, which carries LeRobot's own\n", "install remedy - `train()` reports failure in its result rather than raising, so\n", "an unchecked call would hand Step 6 a `checkpoint_dir` of `None`." ] }, { "cell_type": "code", "execution_count": null, "id": "938c804e27f84196a10c8828c723f798", "metadata": {}, "outputs": [], "source": [ "trainer = create_trainer(\"lerobot_local\", device=\"cpu\")\n", "spec = TrainSpec(\n", " dataset_root=ROOT,\n", " base_model=\"\", # ACT from scratch\n", " output_dir=OUT,\n", " steps=2, # raise to 500+ on a GPU for a real checkpoint\n", " save_freq=2,\n", " global_batch_size=2,\n", " extra={\"policy_type\": \"act\", \"num_workers\": 0},\n", ")\n", "\n", "problems = trainer.validate(spec)\n", "assert not problems, problems\n", "\n", "result = trainer.train(spec)\n", "if result.status != \"success\":\n", " # train() converts any failure into a TrainResult rather than raising, and\n", " # result.message carries the underlying cause - including lerobot's own\n", " # \"'accelerate' is required but not installed\" remedy. Without this check the\n", " # cell prints status=error, and the next cell fails on a None checkpoint_dir\n", " # instead of on the thing that actually went wrong.\n", " raise RuntimeError(f\"training failed: {result.message}\")\n", "print(f\"train status: {result.status}\")\n", "print(f\"checkpoint: {result.checkpoint_dir}\")" ] }, { "cell_type": "markdown", "id": "504fb2a444614c0babb325280ed9130a", "metadata": {}, "source": [ "## 6. Load the trained checkpoint\n", "\n", "The same `create_policy()` entry point loads the checkpoint we just produced.\n", "On hardware you would pass `mode=\"real\"` to deploy against a physical arm." ] }, { "cell_type": "code", "execution_count": null, "id": "59bbdb311c014d738909a11f9e486628", "metadata": {}, "outputs": [], "source": [ "policy = create_policy(result.checkpoint_dir)\n", "print(f\"loaded: {type(policy).__name__}\")\n", "print(\"record -> stream -> train -> load: the data loop closes.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. Same loop, different backend: Isaac (optional - RTX GPU + Isaac Sim)\n", "\n", "The record -> stream loop above ran on the default MuJoCo backend - no GPU, no\n", "credentials. Backend parity for recording landed in #1552 (`IsaacRecordingMixin`),\n", "so the *same* loop runs on NVIDIA Isaac Sim with a single `backend=\"isaac\"` swap.\n", "\n", "This step is optional and self-skips unless you have:\n", "\n", "- an RTX GPU and Isaac Sim 6.0+ installed out-of-band, and\n", "- `pip install -U 'strands-robots[sim-isaac,lerobot]'`.\n", "\n", "One pacing difference from the MuJoCo cells above: Isaac renders at\n", "`rendering_dt = 1/30` by default, so keep `control_frequency <= 1 / rendering_dt`.\n", "We use `fps=10` / `control_frequency=10.0` here (not the MuJoCo cells' `fps=30`)\n", "so every recorded frame reflects a freshly rendered product." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from strands_robots.simulation.isaac import IsaacSimulation\n", "\n", "available, reason = IsaacSimulation.is_available()\n", "if not available:\n", " print(f\"Isaac Sim not available - skipping the backend-swap demo: {reason}\")\n", "else:\n", " ROOT_ISAAC = \"/tmp/nb5_isaac_dataset\"\n", " shutil.rmtree(ROOT_ISAAC, ignore_errors=True)\n", " isaac = Robot(\"so100\", backend=\"isaac\", mesh=False) # same loop, one kwarg changed\n", " isaac.add_camera(name=\"front\", position=[0.5, 0.0, 0.4], target=[0.2, 0.0, 0.05])\n", " isaac.start_recording(\n", " repo_id=\"local/nb5_isaac\",\n", " root=ROOT_ISAAC,\n", " fps=10,\n", " task=\"pick up the red cube\",\n", " cameras=[\"front\"],\n", " overwrite=True,\n", " )\n", " isaac.run_policy(\n", " robot_name=\"so100\",\n", " policy_object=MockPolicy(),\n", " n_steps=20,\n", " control_frequency=10.0,\n", " fast_mode=True,\n", " instruction=\"pick up the red cube\",\n", " )\n", " isaac.stop_recording()\n", " reader = isaac.stream_dataset(\n", " \"local/nb5_isaac\", root=ROOT_ISAAC, shuffle=False, max_num_shards=1, buffer_size=1\n", " )\n", " print(f\"isaac dataset: episodes={reader.num_episodes} frames={reader.num_frames}\")\n", " isaac.destroy()" ] }, { "cell_type": "markdown", "id": "b43b363d81ae4b689946ece5c682cd59", "metadata": {}, "source": [ "## Where to go from here\n", "\n", "- Raise `steps` to 500 and run on a GPU to get a checkpoint that loads and runs.\n", " It will not be a good policy: this notebook records one 60-frame episode from\n", " the Mock policy, so what the run proves is the record-train-load path, not the\n", " behaviour. Collect real demonstrations before you judge a checkpoint.\n", "- Set `BUCKET` and run with `hf auth login` to exercise the full bucket path.\n", "- Swap `mode=\"real\"` on `Robot(\"so100\")` to deploy to a physical SO-101.\n", "- Read the [first post in the series](https://huggingface.co/blog/amazon/strands-lerobot-hub-to-hardware) for the full sim-to-hardware walkthrough.\n", "- See [examples/06_agent_collect_and_stream.py](https://github.com/strands-labs/robots/blob/main/examples/06_agent_collect_and_stream.py) for the agent-driven version." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.12" } }, "nbformat": 4, "nbformat_minor": 5 }