LeRobot으로 시연 데이터를 모았다면 다음 선택지 중 하나가 NVIDIA의 오픈 로봇 파운데이션 모델 GR00T N1.7을 그 데이터로 파인튜닝하는 것입니다. 이 글은 공식 저장소의 문서와 예제 파일을 기준으로 설치부터 데이터 형식, 학습, 오픈루프 평가, 추론 서버 실행까지 순서대로 정리합니다. GR00T가 다른 공개 정책 모델과 비교해 어떤 위치인지는 오픈소스 로봇 정책 모델 비교에서 먼저 볼 수 있습니다.
준비물: GPU, 계정, 소프트웨어
하드웨어 권장 문서와 README에서 확인한 조건은 다음과 같습니다.
| 항목 | 조건 |
|---|---|
| 파인튜닝 GPU | 40GB 이상 1장 최소 (H100, L40, A100 등). 기본 모드 최대 VRAM 약 35GB |
| 권장 구성 | H100 또는 L40 4torchrun 실행 |
| 추론 GPU | 16GB 이상 (RTX 4090, L40, H100, Jetson AGX Thor/Orin, DGX Spark) |
| dGPU 환경 | x86_64, CUDA 12.8, Python 3.12 |
| 계정 | Hugging Face 로그인, nvidia/Cosmos-Reason2-2B 게이트 접근 승인 |
| 도구 | Git LFS, uv, FFmpeg(torchcodec용) |
GR00T N1.7은 3B 파라미터 모델로, 기본 체크포인트는 처음 실행할 때 약 6GB가 내려받아집니다. 학습할 로봇 데이터가 아직 없다면 LeRobot으로 SO-101 데이터를 녹화하는 과정을 먼저 진행합니다. 이 글의 예시는 SO-100/SO-101처럼 팔 관절 5개와 그리퍼 1개, 카메라 2대(전면·손목)를 가진 로봇을 기준으로 합니다.
1단계: 저장소 설치와 동작 확인
README의 설치 순서를 그대로 따릅니다. Git LFS를 먼저 설치하지 않으면 demo_data/의 parquet 파일이 실제 데이터 대신 포인터로 받아집니다.
- Git LFS를 설치하고 서브모듈과 함께 저장소를 클론합니다.
uv와 FFmpeg를 설치합니다.- Python 3.12로 환경을 만들고 설치를 확인합니다.
- Hugging Face에 로그인해 백본 모델 접근을 확인합니다.
sudo apt install git-lfs && git lfs install
git clone --recurse-submodules https://github.com/NVIDIA/Isaac-GR00T
cd Isaac-GR00T
curl -LsSf https://astral.sh/uv/install.sh | sh
sudo apt-get update && sudo apt-get install -y ffmpeg
uv sync --python 3.12
uv run python -c "import gr00t; print('GR00T installed successfully')"
uv run huggingface-cli login # 또는 export HF_TOKEN=<토큰>
학습 단계에서 CUDA_HOME is unset 오류가 나면 README가 안내하는 bash scripts/deployment/dgpu/install_deps.sh를 실행하거나 export CUDA_HOME=/usr/local/cuda로 경로를 지정합니다.
2단계: 데이터를 LeRobot v2 형식으로 맞추기
데이터 준비 문서에 따르면 GR00T는 LeRobot v2 형식을 쓰고, v3 지원은 향후 릴리스로 예고돼 있습니다. 최근 LeRobot으로 녹화한 데이터가 v3라면 저장소의 변환 스크립트를 씁니다. 이 스크립트는 자체 pyproject.toml이 있어서 반드시 해당 폴더 안에서 별도 환경을 만들어 실행해야 하고, 루트에서 설치하면 gr00t 패키지가 대신 설치됩니다.
cd scripts/lerobot_conversion
uv venv && source .venv/bin/activate
uv pip install -e . --verbose
python convert_v3_to_v2.py --repo-id <내_계정>/<데이터셋_이름>
변환 후 폴더 구조는 다음과 같아야 합니다. 에피소드마다 parquet 파일 하나와 카메라별 mp4 파일 하나가 있고, meta/에 GR00T 전용 modality.json이 추가됩니다.
meta/
info.json
episodes.jsonl
tasks.jsonl
modality.json # GR00T 전용
data/chunk-000/episode_000000.parquet
videos/chunk-000/observation.images.front/episode_000000.mp4
videos/chunk-000/observation.images.wrist/episode_000000.mp4
parquet의 observation.state와 action 열은 모든 관절 값을 이어 붙인 1차원 배열이고, modality.json이 이 배열을 이름 붙은 구간으로 나눕니다. 저장소 데모 데이터 cube_to_bowl_5의 실제 파일은 다음과 같습니다.
{
"state": { "single_arm": {"start": 0, "end": 5}, "gripper": {"start": 5, "end": 6} },
"action": { "single_arm": {"start": 0, "end": 5}, "gripper": {"start": 5, "end": 6} },
"video": {
"front": {"original_key": "observation.images.front"},
"wrist": {"original_key": "observation.images.wrist"}
},
"annotation": { "human.task_description": {"original_key": "task_index"} }
}
인덱스는 0부터 시작하고 Python 슬라이스처럼 start는 포함, end는 제외입니다. SO-101은 관절 순서가 어깨 회전, 어깨 들기, 팔꿈치, 손목 굽힘, 손목 회전, 그리퍼이므로 앞의 5개가 single_arm, 마지막 1개가 gripper가 됩니다. 관절 순서가 다른 로봇이라면 info.json의 feature 이름 순서를 보고 구간을 다시 정합니다. 언어 지시 키는 데이터셋 계열에 따라 SO-100형(annotation.human.task_description)과 LIBERO형(annotation.human.action.task_description)으로 나뉘므로 parquet 열, modality.json, 설정 파일 세 곳을 같은 형식으로 맞춥니다.
3단계: 모달리티 설정 파일 작성
새 로봇은 Python 설정 파일로 어떤 관측을 몇 스텝 쓰고 행동을 어떻게 표현할지 정의합니다. 저장소의 so100_config.py가 기본 틀입니다(라이선스 헤더 제외).
from gr00t.configs.data.embodiment_configs import register_modality_config
from gr00t.data.embodiment_tags import EmbodimentTag
from gr00t.data.types import (
ActionConfig, ActionFormat, ActionRepresentation, ActionType, ModalityConfig,
)
so100_config = {
"video": ModalityConfig(delta_indices=[0], modality_keys=["front", "wrist"]),
"state": ModalityConfig(delta_indices=[0], modality_keys=["single_arm", "gripper"]),
"action": ModalityConfig(
delta_indices=list(range(0, 16)), # 16스텝 예측
modality_keys=["single_arm", "gripper"],
action_configs=[
ActionConfig(rep=ActionRepresentation.RELATIVE, type=ActionType.NON_EEF,
format=ActionFormat.DEFAULT), # 팔: 현재 상태 대비 상대값
ActionConfig(rep=ActionRepresentation.ABSOLUTE, type=ActionType.NON_EEF,
format=ActionFormat.DEFAULT), # 그리퍼: 절대 목표값
],
),
"language": ModalityConfig(delta_indices=[0],
modality_keys=["annotation.human.task_description"]),
}
register_modality_config(so100_config, embodiment_tag=EmbodimentTag.NEW_EMBODIMENT)
이 설정에서 바꿀 지점은 세 가지입니다. modality_keys는 modality.json의 키 이름과 정확히 같아야 하고, action_configs는 행동 키 순서대로 하나씩 둡니다. 파인튜닝 문서는 팔 관절을 상대값으로 두는 이유를 일반화가 더 낫기 때문이라고, 그리퍼를 절대값으로 두는 이유를 열고 닫는 이진 동작에 더 잘 맞기 때문이라고 설명합니다. 사용자 정의 로봇은 항상 EmbodimentTag.NEW_EMBODIMENT로 등록합니다.
4단계: 파인튜닝 실행
단일 GPU 예시는 다음과 같습니다. 저장소의 데모 데이터로 먼저 한 번 돌려 환경을 확인한 뒤 --dataset-path를 내 데이터로 바꿉니다.
CUDA_VISIBLE_DEVICES=0 uv run python \
gr00t/experiment/launch_finetune.py \
--base-model-path nvidia/GR00T-N1.7-3B \
--dataset-path demo_data/cube_to_bowl_5 \
--embodiment-tag NEW_EMBODIMENT \
--modality-config-path examples/SO100/so100_config.py \
--num-gpus 1 \
--output-dir /tmp/so100 \
--save-steps 500 \
--max-steps 2000 \
--global-batch-size 32 \
--dataloader-num-workers 4
주요 옵션은 --help로 전체 목록을 볼 수 있습니다. 검증 손실을 보고 싶다면 --eval-strategy steps --eval-steps 500을 추가합니다(기본은 꺼짐). GPU가 적으면 그래디언트 누적으로 전역 배치를 맞추는데, 하드웨어 문서의 예시는 GPU 4장 × GPU당 배치 8 × 누적 8스텝으로 전역 배치 256을 만드는 구성입니다. 시스템 RAM이 부족하면 --num-shards-per-epoch를 낮춥니다. 이 값은 GPU 메모리가 아니라 RAM에 미리 올리는 데이터 양을 정합니다. 기본 모드는 projector와 DiT 행동 헤드만 학습하고, --tune-llm이나 --tune-visual을 켜면 GPU당 80GB 이상이 권장됩니다.
5단계: 오픈루프 평가로 학습 상태 확인
실제 로봇에 올리기 전에 학습 데이터의 궤적을 넣어 예측 행동이 실제 행동을 따라가는지 봅니다.
uv run python gr00t/eval/open_loop_eval.py \
--dataset-path ./demo_data/cube_to_bowl_5 \
--embodiment-tag NEW_EMBODIMENT \
--model-path /tmp/so100/checkpoint-2000 \
--traj-ids 0 \
--execution-horizon 16 \
--steps 400 \
--modality-keys single_arm gripper \
--save-plot-path ./eval_plots
판단 기준은 문서가 제시하는 세 가지입니다. 첫째, 학습에 쓴 궤적에서 예측 곡선이 실제 곡선과 겹쳐야 하고, 평평한 직선으로 나오면 설정 문제입니다. 둘째, --save-steps 500으로 저장한 중간 체크포인트를 차례로 평가해 Average MSE가 내려가는지 봅니다. 문서의 참고 실행에서는 500스텝 87.5, 1000스텝 25.4, 1500스텝 13.2, 2000스텝 10.0이었고, 문서는 이 값이 목표치가 아니라고 명시합니다. 셋째, 수정하지 않은 명령으로 얻은 첫 결과를 기준선으로 기록하고 이후 변경을 그 기준선과 비교합니다.
자주 막히는 지점과 원인
| 증상 | 가능성이 큰 원인 | 확인할 곳 |
|---|---|---|
| MSE가 단계가 지나도 줄지 않음 | 학습률이 낮거나 데이터 로딩 문제 | 데이터 경로, 에피소드 수, 학습률 |
| 예측이 상수처럼 일정함 | modality.json 키와 설정 파일 키 불일치, 설정 파일 경로 오류 | modality_keys, --modality-config-path |
| 예측 값이 매우 크거나 NaN | 정규화 통계 문제 | meta/stats.json, meta/relative_stats.json |
| 첫 실행에서 모델 다운로드 실패 | Cosmos-Reason2-2B 접근 미승인 | Hugging Face 모델 페이지 접근 요청 |
| parquet 읽기 오류 | Git LFS 없이 클론 | git lfs install 후 git lfs pull |
6단계: 추론 서버로 로봇에 연결하기
정책 API 문서는 GPU 서버에서 정책을 띄우고 로봇 쪽에서 클라이언트로 행동을 받아 오는 구조를 안내합니다.
uv run python gr00t/eval/run_gr00t_server.py \
--embodiment-tag NEW_EMBODIMENT \
--model-path /tmp/so100/checkpoint-2000 \
--device cuda:0 --host 0.0.0.0 --port 5555
from gr00t.policy.server_client import PolicyClient
policy = PolicyClient(host="localhost", port=5555, timeout_ms=15000, strict=False)
if not policy.ping():
raise RuntimeError("Cannot connect to policy server!")
observation = {
"video": {"front": front_frames, "wrist": wrist_frames}, # (1, T, H, W, 3) uint8
"state": {"single_arm": arm_state, "gripper": grip_state}, # (1, T, D) float32
"language": {"task": [["put the cube into the yellow bowl"]]},
}
action, info = policy.get_action(observation)
next_arm = action["single_arm"][:, 0, :] # (B, T, D) 중 첫 스텝
반환되는 행동은 정규화되지 않은 물리 단위이고, (배치, 시간, 차원) 형태의 행동 묶음입니다. 모델 카드에 따르면 TensorRT 전체 파이프라인 기준 L40에서 26.0Hz, Jetson AGX Thor에서 10.7Hz로 행동 묶음을 다시 계산하며, 하드웨어 문서는 행동 묶음 덕분에 약 10Hz 재계획으로 약 30FPS 로봇 실행이 가능하다고 설명합니다. 로봇 탑재 장치를 고르는 기준은 VLA 엣지 하드웨어 산정 가이드에서, GR00T가 NVIDIA의 학습·시뮬레이션·배포 흐름에서 어디에 놓이는지는 Cosmos·Isaac·GR00T 3-컴퓨터 구조에서 이어서 볼 수 있습니다.