Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Build and Push Gr00t Images

on:
push:
branches:
- main-positronic

jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
Comment on lines +12 to +13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check out submodules before packaging the fork

When this workflow builds the published image, actions/checkout leaves submodules uninitialized by default (its submodules input defaults to false), so Docker's COPY . /gr00t packages empty external_dependencies paths; .dockerignore also strips .git, preventing /gr00t from initializing them later. The LIBERO and SimplerEnv setup scripts immediately run git submodule update, which then fails because the included checkout is not a Git repository, making simulation evaluation unavailable from this image. Configure checkout with submodules: recursive before make push.

Useful? React with 👍 / 👎.


- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf "${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/CodeQL"
sudo rm -rf "${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/node"
sudo rm -rf "${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/go"
sudo rm -rf /usr/lib/jvm
sudo docker system prune -af || true
df -h

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build and Push
working-directory: docker
run: |
make push
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ See the [Orin setup guide](scripts/deployment/README.md#jetson-orin-setup) for D
> per-platform Docker and bare-metal setup.


For a containerized setup that avoids system-level dependency conflicts, see our [Docker Setup Guide](docker/README.md). The recommended container workflow is to start the image first, then clone or pull the repo inside the running container so your checkout uses the image's prebuilt dependency environment.
For a containerized setup that avoids system-level dependency conflicts, see our [Docker Setup Guide](docker/README.md). The image includes this fork at `/gr00t` with its prebuilt dependency environment.

---

Expand Down
6 changes: 6 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,10 @@ ENV MUJOCO_GL="egl" \
# bootstrap_wheels.sh before sync (then committed back) if missing. flash-attn
# comes from official release URLs — no source build needed.

# Install the fork alongside its dependency environment for Positronic subprocesses.
WORKDIR /gr00t
COPY . /gr00t
RUN uv pip install --python /opt/gr00t-venv/bin/python --no-deps -e /gr00t
ENV PYTHONPATH=/gr00t

CMD ["/bin/bash"]
79 changes: 79 additions & 0 deletions docker/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
.PHONY: all build tag push clean prune help

# Image configuration
IMAGE_NAME_GROOT := positro/gr00t-base

# Extract version from pyproject.toml (first literal version entry)
VERSION := $(shell sed -n 's/^version = "\([^"]*\)"/\1/p' ../pyproject.toml | head -n 1)

GIT_SHA := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")

REGISTRY_URL ?= docker.io

TAG_GROOT_LATEST := $(IMAGE_NAME_GROOT):latest
TAG_GROOT_VERSION := $(IMAGE_NAME_GROOT):v$(VERSION)
TAG_GROOT_SHA := $(IMAGE_NAME_GROOT):$(GIT_SHA)
LOCAL_TAG_GROOT := $(IMAGE_NAME_GROOT):local

help:
@echo "gr00t Groot Docker Build System"
@echo ""
@echo "Configuration:"
@echo " Groot Image: $(IMAGE_NAME_GROOT)"
@echo " Version: $(VERSION)"
@echo " Git SHA: $(GIT_SHA)"
@echo " Registry URL: $(REGISTRY_URL)"
@echo ""
@echo "Targets:"
@echo " make build Build the groot image"
@echo " make tag Tag the groot image"
@echo " make push Push groot tags to Docker Hub"
@echo " make clean Remove local groot images"
@echo " make prune Remove dangling/unused Docker images"
@echo " make help Show this help message"
@echo ""

build:
@echo "Building $(IMAGE_NAME_GROOT) image..."
@if [ -z "$(VERSION)" ]; then \
echo "Error: Could not extract version from pyproject.toml"; \
exit 1; \
fi
docker build --platform linux/amd64 \
-f Dockerfile \
-t $(LOCAL_TAG_GROOT) ..

tag: build
@echo "Tagging groot image with multiple tags..."
docker tag $(LOCAL_TAG_GROOT) $(TAG_GROOT_LATEST)
docker tag $(LOCAL_TAG_GROOT) $(TAG_GROOT_VERSION)
docker tag $(LOCAL_TAG_GROOT) $(TAG_GROOT_SHA)
@echo "Tagged with:"
@echo " - $(TAG_GROOT_LATEST)"
@echo " - $(TAG_GROOT_VERSION)"
@echo " - $(TAG_GROOT_SHA)"

push: tag
@echo "Pushing groot images to Docker Hub..."
docker push $(TAG_GROOT_LATEST)
docker push $(TAG_GROOT_VERSION)
docker push $(TAG_GROOT_SHA)
@echo ""
@echo "Successfully pushed groot images to Docker Hub!"
@echo "To use on cloud instances, set:"
@echo " export IMAGE_REGISTRY=$(REGISTRY_URL)/"

all: push

clean:
@echo "Removing local groot images..."
-docker rmi $(LOCAL_TAG_GROOT)
-docker rmi $(TAG_GROOT_LATEST)
-docker rmi $(TAG_GROOT_VERSION)
-docker rmi $(TAG_GROOT_SHA)
@echo "Cleanup complete."

prune:
@echo "Pruning dangling and unused Docker images..."
docker image prune -f
@echo "Prune complete."
35 changes: 30 additions & 5 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,38 @@ From the repository root:
bash docker/build.sh
```

This builds from `nvidia/cuda:12.8.0-devel-ubuntu24.04` and installs all dependencies into `/opt/gr00t-venv`. The image does not include a working source checkout; for normal use, start the image and then clone or pull the repo you want to run inside the container.
This builds from `nvidia/cuda:12.8.0-devel-ubuntu24.04` and installs all dependencies into `/opt/gr00t-venv`. The image includes this fork at `/gr00t`, installed into `/opt/gr00t-venv`.

## Positronic base image

```bash
make -C docker build
make -C docker push
```

The Makefile publishes `positro/gr00t-base` with `latest`, version, and commit tags.
It targets `linux/amd64`, including when built on an Apple Silicon Mac.
For a native ARM build, use `bash docker/build.sh` on the target host.
Positronic's `GROOT_BASE_IMAGE` selects an existing base image for its adapter build.

Fine-tuning defaults to the base checkpoint's saved model and modality configuration.
The Python launcher accepts `--video-keys` to select the fine-tuning camera layout; omitted, it retains the checkpoint's views.
When using `examples/finetune.sh`, put this option after the script's `--` passthrough delimiter:

```bash
bash examples/finetune.sh \
--base-model-path nvidia/GR00T-N1.7-DROID \
--dataset-path /data/droid \
--embodiment-tag oxe_droid_relative_eef_relative_joint \
--output-dir /data/checkpoints \
-- --video-keys exterior_image_1_left exterior_image_2_left wrist_image_left
```

The policy server accepts `--model-path hf://nvidia/GR00T-N1.7-DROID` and downloads that snapshot.

## Running the Container

**Recommended workflow: run the image, then clone or update the repo inside it.**
**Run the included fork from `/gr00t`.**

Start an interactive shell:

Expand All @@ -35,9 +62,7 @@ docker run -it --rm --gpus all \
Then, inside the container:

```bash
git clone --recurse-submodules https://github.com/NVIDIA/Isaac-GR00T /workspace/Isaac-GR00T
cd /workspace/Isaac-GR00T
export PYTHONPATH="$PWD${PYTHONPATH:+:$PYTHONPATH}"
cd /gr00t
python -c "import gr00t; print('GR00T ready')"
```

Expand Down
7 changes: 5 additions & 2 deletions gr00t/configs/finetune_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ class FinetuneConfig:

modality_config_path: str | None = None
"""
Path to a Python file defining the modality configuration for the given embodiment.
If None, use the pre-registered modality config in `gr00t/configs/data/embodiment_configs.py`.
Path to a Python file defining the modality configuration for the given embodiment.
If None, retain the modality configuration saved in the base checkpoint.
"""

video_keys: list[str] | None = None
"""Explicit camera keys for the fine-tuning dataset; omission retains the checkpoint's cameras."""
Comment thread
vertix marked this conversation as resolved.

# --- Model Tuning Flags ---
tune_llm: bool = False
"""If True, fine-tune the language model (LLM) backbone during training."""
Expand Down
4 changes: 4 additions & 0 deletions gr00t/data/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
from gr00t.data.embodiment_tags import EmbodimentTag


VIDEO = "video"
LANGUAGE = "language"


class MessageType(Enum):
START_OF_EPISODE = "start_of_episode"
END_OF_EPISODE = "end_of_episode"
Expand Down
54 changes: 39 additions & 15 deletions gr00t/experiment/launch_finetune.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@
# Launch finetuning for N1.7 on "single node".
# This script tries to provide a similar user experience as current OSS.

import copy
import json
import os
from pathlib import Path

from transformers.utils import cached_file
import tyro

from gr00t.configs.base_config import get_default_config
from gr00t.configs.finetune_config import FinetuneConfig
from gr00t.experiment.experiment import run
from gr00t.configs.model.gr00t_n1d7 import Gr00tN1d7Config
from gr00t.data.embodiment_tags import EmbodimentTag
from gr00t.data.types import ModalityConfig


# Make sure the user provided modality config is registered.
Expand All @@ -41,16 +45,9 @@ def load_modality_config(modality_config_path: str):
raise FileNotFoundError(f"Modality config path does not exist: {modality_config_path}")


if __name__ == "__main__":
# Set LOGURU_LEVEL environment variable if not already set (default: INFO)
if "LOGURU_LEVEL" not in os.environ:
os.environ["LOGURU_LEVEL"] = "INFO"
# Use tyro for clean CLI
ft_config = tyro.cli(FinetuneConfig, description=__doc__)
from gr00t.data.embodiment_tags import EmbodimentTag

ft_config.embodiment_tag = EmbodimentTag.resolve(ft_config.embodiment_tag)
embodiment_tag = ft_config.embodiment_tag.value
def build_config(ft_config: FinetuneConfig):
"""Inherit model and modality contracts from the checkpoint before applying training overrides."""
embodiment_tag = EmbodimentTag.resolve(ft_config.embodiment_tag).value

# all rank workers should register for the modality config
if ft_config.modality_config_path is not None:
Expand All @@ -73,21 +70,42 @@ def load_modality_config(modality_config_path: str):
}
)
config.load_config_path = None
config.model = Gr00tN1d7Config.from_pretrained(ft_config.base_model_path)
checkpoint = Path(ft_config.base_model_path)
processor_root = checkpoint / "processor" if (checkpoint / "processor").is_dir() else checkpoint
processor_file = cached_file(str(processor_root), "processor_config.json")
with open(processor_file) as f:
processor_kwargs = json.load(f)["processor_kwargs"]
config.model.use_relative_action = processor_kwargs["use_relative_action"]
if ft_config.modality_config_path is None:
modalities = processor_kwargs["modality_configs"][embodiment_tag]
config.data.modality_configs = {
embodiment_tag: {name: ModalityConfig(**value) for name, value in modalities.items()}
}
else:
config.data.modality_configs = copy.deepcopy(config.data.modality_configs)
if ft_config.video_keys is not None:
if not ft_config.video_keys or len(set(ft_config.video_keys)) != len(ft_config.video_keys):
raise ValueError("video_keys must be nonempty and unique")
config.data.modality_configs[embodiment_tag]["video"].modality_keys = ft_config.video_keys

# overwrite with finetune config supplied by the user
config.model.tune_llm = ft_config.tune_llm
config.model.tune_visual = ft_config.tune_visual
config.model.tune_projector = ft_config.tune_projector
config.model.tune_diffusion_model = ft_config.tune_diffusion_model
config.model.state_dropout_prob = ft_config.state_dropout_prob
config.model.random_rotation_angle = ft_config.random_rotation_angle
config.model.color_jitter_params = ft_config.color_jitter_params
if ft_config.random_rotation_angle is not None:
config.model.random_rotation_angle = ft_config.random_rotation_angle
if ft_config.color_jitter_params is not None:
config.model.color_jitter_params = ft_config.color_jitter_params
config.model.use_percentiles = ft_config.use_percentiles
Comment thread
vertix marked this conversation as resolved.
if (ft_config.shortest_image_edge is None) != (ft_config.crop_fraction is None):
raise ValueError("shortest_image_edge and crop_fraction must be set together")
if ft_config.shortest_image_edge is not None:
config.model.shortest_image_edge = ft_config.shortest_image_edge
config.model.crop_fraction = ft_config.crop_fraction
if config.model.shortest_image_edge is not None and config.model.crop_fraction is not None:
config.model.image_crop_size = None
config.model.image_target_size = None
if ft_config.extra_augmentation_config:
Expand All @@ -99,7 +117,6 @@ def load_modality_config(modality_config_path: str):
config.model.reproject_vision = False
config.model.model_name = "nvidia/Cosmos-Reason2-2B"
config.model.backbone_trainable_params_fp32 = True
config.model.use_relative_action = True

config.training.experiment_name = ft_config.experiment_name
config.training.start_from_checkpoint = ft_config.base_model_path
Expand Down Expand Up @@ -127,4 +144,11 @@ def load_modality_config(modality_config_path: str):
config.training.resume_from_checkpoint = ft_config.resume_from_checkpoint
config.training.skip_weight_loading = ft_config.skip_weight_loading

run(config)
return config


if __name__ == "__main__":
from gr00t.experiment.experiment import run

os.environ.setdefault("LOGURU_LEVEL", "INFO")
run(build_config(tyro.cli(FinetuneConfig, description=__doc__)))
2 changes: 2 additions & 0 deletions gr00t/model/gr00t_n1d7/processing_gr00t_n1d7.py
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,8 @@ def from_pretrained(cls, pretrained_model_name_or_path: str | Path, **kwargs):
"exclude_state",
"state_dropout_prob",
"use_mean_std",
"use_percentiles",
"extra_augmentation_config",
"model_name",
"model_type",
"max_action_horizon",
Expand Down
Loading