# 롤아웃 PC에서 PI052를 돌릴 준비 (한 번만) `leapshared/Redox_200epi_subtask_pi052`를 OpenArm 실물 로봇에서 `lerobot-rollout`으로 돌리기 위한 설치·확인 절차다. 학습 서버 쪽에서 글자 그대로 돌려 본 것이고(아래 "확인한 범위"), 롤아웃 PC에서 사람이 하든 그 PC의 코딩 어시스턴트(Claude Code 등)에게 시키든 이 문서 하나로 되게 적었다. ## 어시스턴트에게 시킬 때의 규칙 - **`lerobot-rollout`(실물 로봇)은 운영자가 직접 실행한다.** 실물 팔이 움직이는 경로다. 어시스턴트는 1번(설치)과 2번(확인)까지만 하고, 3번 명령은 만들어 건네기만 한다. - 기존 `.venv`(lerobot 0.6.1, GR00T용)는 건드리지 않는다. **`uv sync`를 돌리지 않는다**(`lerobot[groot]`로 얹은 패키지가 지워진다). `pip install`도 섞지 않는다. - lerobot 브랜치 소스는 1번의 패치 두 줄 말고는 고치지 않는다. 0.6.1에 `pi052`를 옮겨 심으려 하지 않는다. - 막히면 추측으로 버전을 바꾸지 말고 출력의 마지막 20줄을 운영자에게 보여 준다. ## 왜 따로 깔아야 하나 이 모델은 lerobot PR [#4184](https://github.com/huggingface/lerobot/pull/4184)의 브랜치 `codex/pi052-language-policy`(커밋 `3298e15`, lerobot 0.6.2)에만 있는 `pi052` 정책이고, 서브태스크를 스스로 만들게 하는 `--interactive`도 그 브랜치에만 있다. 0.6.1의 `lerobot-rollout`으로 3번 명령을 돌리면 이렇게 끝난다. lerobot-rollout: error: unrecognized arguments: --interactive=true --autosteer_interval_s=3 프롬프트가 `(lerobot-robot-openarm-sciedu)`이면 0.6.1의 `.venv`가 켜져 있는 것이다. 켜져 있어도 상관없다. 아래처럼 `.venv_pi052/bin/...`을 경로로 적어 부르면 된다. ## 1. 설치 (`~/Documents/openarmsciedu`에서) ```bash git clone --single-branch --branch codex/pi052-language-policy https://github.com/huggingface/lerobot.git ../lerobot_pi052 git -C ../lerobot_pi052 checkout 3298e15a9e897b09b582b4ec38786ecdbedb1630 uv venv --python 3.12 .venv_pi052 uv pip freeze --python .venv/bin/python | grep -v -i -E "^lerobot|^-e " > /tmp/pi052_constraints.txt uv pip install --no-sources --python .venv_pi052/bin/python -c /tmp/pi052_constraints.txt \ -e "../lerobot_pi052[pi,dataset,openarms,viz,deepdiff-dep,pynput-dep]" "mujoco==3.11.0" "openarm-mujoco==2.1.0" \ pyrealsense2 aiohttp pyyaml opencv-python-headless \ "openarm-control @ git+https://github.com/enactic/openarm_control@7867db88ef4814413ad1ee20d6aa82c92a167417" uv pip install --no-deps --python .venv_pi052/bin/python -e . .venv_pi052/bin/hf download leapshared/Redox_200epi_subtask_pi052 pi052_embed_prefix.patch rollout_pi052.py redox_subtasks.txt autosteer_goal.txt autosteer_line.txt --local-dir scripts/pi052 git -C ../lerobot_pi052 apply "$PWD/scripts/pi052/pi052_embed_prefix.patch" ``` - 넷째 줄은 지금 되는 `.venv`의 버전(torch 2.11.0, transformers 5.5.4 등)을 그대로 묶는다. RTX 5090에서 이미 도는 torch를 그대로 쓰려는 것이다. - `.venv`에 URL이나 파일 경로로 깐 패키지가 있으면(예: `flash-attn @ file:///tmp/...whl`) 그 줄도 constraints에 들어가지만, 쓰이지 않는 URL 제약은 uv가 검사하지 않으므로 그대로 둬도 풀린다(롤아웃 PC에서 확인). - `--no-sources`는 빼면 안 된다. 브랜치의 `[tool.uv.sources]`가 torch를 cu128 인덱스로 강제해서 위 묶음과 충돌한다. - `mujoco==3.11.0`, `openarm-mujoco==2.1.0`도 빼면 안 된다. 묶지 않으면 3.13 / 2.3이 깔리고 시뮬 로봇이 `ArmConfigurationLimit only supports scalar joints`로 죽는다. - 마지막 두 줄은 그 커밋의 액션 추론 버그(`PI05Pytorch.embed_prefix() takes 5 positional arguments but 7 were given`)를 고치는 두 줄짜리 패치다. 이미 적용되어 있으면 `git apply`가 "patch does not apply"로 실패한다. 그때는 `git -C ../lerobot_pi052 diff --stat`에 `modeling_pi052.py`가 보이면 된 것이다. ## 2. 확인 (로봇을 켜기 전에) ```bash .venv_pi052/bin/lerobot-rollout --help | grep -c -- "--interactive" .venv_pi052/bin/hf auth whoami .venv_pi052/bin/hf download google/paligemma-3b-pt-224 tokenizer.json tokenizer.model tokenizer_config.json special_tokens_map.json added_tokens.json config.json .venv_pi052/bin/hf download leapshared/Redox_200epi_subtask_pi052 --exclude "checkpoints/*" ``` - 첫 줄은 1 이상이어야 한다. 0이면 0.6.1의 것이 불린 것이다. - **셋째 줄이 중요하다.** 전처리기는 실행할 때 `google/paligemma-3b-pt-224`의 토크나이저를 허브에서 받는데, 그 저장소는 승인이 필요하다(gated). 토큰 없이는 `GatedRepoError 401`이다. `lerobot-rollout`은 정책을 불러오고 → 로봇을 연결해 홈으로 보낸 **뒤에** → 전처리기를 만든다. 그래서 못 받으면 팔이 홈까지 간 뒤에 프로그램이 죽는다 (플러그인의 `atexit` 뒷문이 팔을 내려 주므로 위험하지는 않다. 한 번을 버릴 뿐이다). 실패하면 에서 약관에 동의한 계정으로 `.venv_pi052/bin/hf auth login` 한다. 한 번 받아 두면 캐시에서 읽는다. - 넷째 줄은 모델을 미리 받아 둔다(16.6 GB). `--exclude "checkpoints/*"`를 빼면 체크포인트 넷까지 83 GB를 받는다. - 로봇 없이 끝까지 보고 싶으면 3번 명령에서 `--robot.type=openarm_sciedu_sim --robot.viewer=false --robot.render_camera=true --display_data=false`로만 바꾼다. 시뮬 장면에는 비커도 판도 없어서 생성되는 문장은 내내 "return to the home posture"다. 그게 정상이다. ## 3. 실행 (운영자가 한다) ```bash .venv_pi052/bin/lerobot-rollout \ --robot.type=openarm_sciedu \ --robot.config_path=config/robot_a.yaml \ --robot.arms=left,right \ --robot.go_home=true \ --robot.entry_ramp_s=2.0 \ --robot.max_start_mismatch_deg=45.0 \ --policy.path=leapshared/Redox_200epi_subtask_pi052 \ --policy.dtype=bfloat16 \ --strategy.type=base \ --inference.type=sync \ --task="lift the metal plate out of the left beaker" \ --interactive=true \ --autosteer_interval_s=3 \ --fps=30 \ --return_to_initial_position=true \ --display_data=true \ --play_sounds=false \ --device=cuda ``` **돌리기 전에 `nvidia-smi`를 본다.** 모델을 불러오는 도중의 피크가 24.2 GiB(실제 할당)라서 32 GB 카드에서는 약 25.5 GiB가 비어 있어야 한다. 모델을 쥔 채 살아 있는 다른 파이썬 프로세스가 있으면 로딩에서 OOM이 난다(로딩은 로봇 연결보다 앞이라 팔이 움직이기 전이다). 불러온 뒤에도 이 프로세스는 약 25 GiB로 보인다(캐시다. 정상이다). **배너(`/start`, `/subtask`, ... 명령 목록)가 뜰 때까지 아무것도 입력하지 않는다.** 모델을 불러오는 1분 반쯤은 로그가 없어서(`Loading policy from ...`와 liger 경고에서) 멈춘 것처럼 보이는데 정상이다. 그동안 친 줄은 버려지지 않는다. 터미널의 입력 큐에 남아 있다가 세션이 시작되는 순간 읽힌다. 로딩 중에 `/start`를 쳐 두면 홈 이동이 끝나자마자, 배너의 "the robot will NOT move until you type /start"와 달리 아무것도 치지 않았는데 롤아웃이 시작된다(2026-09-21에 실물에서 있었던 일이고, 시뮬에서 가짜 터미널로 재현했다: 배너와 `Rollout running`이 같은 순간에 찍힌다. 상류가 세션 시작 때 입력 큐를 비우지 않는다). 잘못 쳤으면 로봇이 연결되기 전(로딩 중)에 Ctrl-C 하는 것이 가장 깨끗하다. 배너가 뜨면 `/start`를 치고, 바로 이어서 아래 줄을 붙여넣는다(미리 복사해 둔다. `/autosteer`는 `/start` 뒤에만 받는다). **이 줄을 터미널 화면에서 복사하지 않는다.** 164자라서 화면에서는 접혀 보이고, 접힌 줄을 복사하면 두 줄로 붙는다. `scripts/pi052/autosteer_line.txt`를 에디터로 열어 복사한다. 붙여넣은 뒤 `Autosteer on — goal '...'`이 **`on the left.`로 끝나는지** 본다. 잘렸으면 같은 줄을 다시 넣으면 된다(새 목표가 앞의 것을 대신한다). 실물 첫 실행에서 목표가 `... then put`에서 잘려 들어갔고 목록에 없는 문장('lift the plate out of the left beaker', 'raise raise')이 나왔다. 같은 프레임으로 재 보니 전체 문장은 목록 밖 0번(334회 중), 잘린 문장은 46번이었고 실물에서 나온 것과 같은 문장들이었다. 텍스트 헤드는 이 task 문장 하나로만 학습했다. ```text /autosteer Hold the metal plate by the orange handle, dip it into the blue solution, take it out and point at it, then put the plate back in the beaker on the left. ``` - `/start` 뒤 약 2초는 아무 일도 없는 것처럼 보인다. `sync`에서는 첫 청크를 계산하는 동안 명령이 나가지 않고 로봇은 마지막 명령(홈 자세)을 유지한다. 그다음 2초에 걸쳐 부드럽게 출발한다 (플러그인의 진입 램프는 첫 `send_action`에서 시작하므로 GR00T 때와 똑같이 걸린다. 프로세스당 한 번이라 두 번째 `/start`에는 없다). 첫 `/autosteer` 질의 때 한 번 더 약 2초 멈추고, 그 뒤로는 청크 경계(50액션마다)에서 약 0.15초, 3초마다의 질의에서 약 0.12초 멈칫한다(RTX 5090에서 잰 추론 시간이다). - `--task`는 `/start` 순간에 액션 쪽이 받는 첫 서브태스크 문장이다. 전체 task 문장은 `/autosteer`에 준다. 액션 쪽은 서브태스크 8문장으로만 학습했으므로 `--task`에 전체 문장을 넣지 않는다. `/autosteer`를 치지 않으면 "lift ..." 하나만 끝까지 따른다. - GR00T 때 쓰던 명령과 로봇 쪽 여섯 줄, `strategy`, `inference`, `fps`부터 `device`까지는 글자까지 같다. GR00T의 task 문장("Dip the metal plate ... five times")은 가져오지 않는다. - `--inference.rtc.mode=trained`는 쓸 수 없다(RTC로 학습한 체크포인트 전용. 시작할 때 거부된다). RTC를 쓰려면 `--inference.type=rtc --inference.rtc.execution_horizon=10`만 바꿔 얹고 `--inference.queue_threshold`는 기본값(30)으로 둔다. ### 3-1. 런처로 돌리기 (실물 첫 실행에서 나온 문제들의 대책. 인자는 같고 첫 줄만 다르다) ```bash PI052_SUBTASK_ORDER=scripts/pi052/redox_subtasks.txt PI052_AUTOSTEER_GOAL=scripts/pi052/autosteer_goal.txt \ .venv_pi052/bin/python scripts/pi052/rollout_pi052.py \ --robot.type=openarm_sciedu \ --robot.config_path=config/robot_a.yaml \ --robot.arms=left,right \ --robot.go_home=true \ --robot.entry_ramp_s=2.0 \ --robot.max_start_mismatch_deg=45.0 \ --policy.path=leapshared/Redox_200epi_subtask_pi052 \ --policy.dtype=bfloat16 \ --strategy.type=base \ --inference.type=sync \ --task="lift the metal plate out of the left beaker" \ --interactive=true \ --autosteer_interval_s=3 \ --fps=30 \ --return_to_initial_position=true \ --display_data=true \ --play_sounds=false \ --device=cuda ``` 배너가 뜬 뒤에 `/start`, 이어서 `/autosteer go`(뒤에 무엇을 치든 `autosteer_goal.txt`의 문장이 목표가 되고, `Autosteer on — goal`에 그 문장이 찍힌다). - 배너 직전에 터미널 입력 큐를 비운다. 로딩 중에 친 `/start`가 배너와 동시에 읽혀 출발하는 일이 없다. - 긴 `/autosteer` 줄을 붙여넣을 일이 없다(잘릴 일이 없다). - `PI052_SUBTASK_ORDER`: 목록(8문장)에 없는 생성 문장을 거른다. 목록의 문장과 거의 같으면(글자 유사도 0.85 이상) 그 문장으로 맞추고, 아니면 버리고 지금 문장을 유지한다. 거른 것은 `[pi052] ...`로 찍힌다. 액션 쪽은 목록의 문장으로만 학습했으므로 'raise raise' 같은 문장이 프롬프트로 들어가면 거동을 알 수 없다. 전체 목표 문장에서는 목록 밖 문장이 0/334였으므로 목표만 제대로 들어가면 급한 장치는 아니다. `PI052_AUTOSTEER_GOAL`만 주고 돌려도 된다. - `PI052_SUBTASK_CONFIRM_JUMPS=1`(기본은 꺼짐. 위 목록이 있어야 한다): 지금 단계에서 0~+2단계가 아닌 답(세 단계 이상 앞으로 뛰거나 뒤로 돌아가는 것)은 같은 답이 두 번 연달아 나와야 받는다. 한 장면으로 구별되지 않는 쌍(3번 "point at ..."과 6번 "raise ... and point", 1번과 7번)에서 고수준이 한 번씩 튀는 것을 거른다. 어떤 답이든 두 번 연달아 나오면 통과하므로 잠기지 않는다. +2까지 바로 받는 것은 2번(시연에서 2.2초)과 4번(2.6초)이 3초 질의 간격에서 한 번도 안 잡힐 수 있어서다. **처음에 넣었던 "앞 단계 문장은 버린다"는 없앴다.** 앞으로 잘못 뛴 답 하나를 받으면 그 뒤의 옳은 답이 전부 "앞 단계"라 버려져서, 3초 뒤에 저절로 고쳐질 오답이 운영자가 손댈 때까지 굳는다 (철판을 쥔 채 'return ...'에 굳으면 용액 위에서 철판을 놓을 수 있다). 학습에 쓰지 않은 영상의 흐름을 다시 돌려 보니 실제로 4번을 건너뛰고 7번에 굳었다. - 같은 서브태스크는 다시 찍지 않는다(문장이 바뀔 때만 찍는다). 3초마다 찍히는 줄이 입력 도중에 끼어들어 `/stop`을 못 친 일이 있었다. - 시뮬에서 가짜 터미널로 확인했다: 미리 친 `/start`가 버려지고, `/autosteer go`의 목표가 `on the left.`로 끝나고, 'lift the plate out of the left beaker'는 1번으로 맞춰지고, 'raise raise'와 'hold the plate'는 버려지고, 2번에서 한 번 튄 'return to the home posture'는 적용되지 않는다. ## 4. 멈출 때 - **철판을 쥐고 있거나 철판이 용액·비커 안에 있는 동안에는 `/stop`, `/reset`, Ctrl-C를 하지 않는다.** 셋 다 먼저 처음 자세로 3초 동안 관절 공간에서 직선으로 가고, 그리퍼도 처음 값(열림)으로 같이 돌아가므로 가는 도중에 철판을 놓는다. `/stop`과 Ctrl-C는 그 뒤에 팔을 내리고 토크를 푼다. 철판이 왼쪽 비커에 돌아가고 팔이 홈에 온 뒤에 `/stop`한다. 급하면 비상정지다. - 고수준이 한 문장에 머물거나 앞 단계로 되돌아가면 손으로 넘긴다(autosteer는 꺼진다. 문장이 바뀌면 큐에 남은 액션을 버리고 바로 새로 추론한다). `/subtask raise the plate and point at it` → `/subtask put the plate into the left beaker` → `/subtask return to the home posture` → `/stop`. 다시 맡기려면 위 `/autosteer` 줄을 다시 친다. - 입력 도중에 출력이 끼어들어 화면이 깨져도 친 글자는 입력 줄에 그대로 들어가 있다. 다시 치면 조각이 쌓인다(`Unknown command '//st/st/autosteer'`). Ctrl-U로 줄을 비우고 `/stop`을 친다. - 끝난 뒤에는 'return to the home posture'에 머문다. 시연은 홈까지 돌아가지 않고 홈에서 관절 기준 약 21도 앞(200개의 중앙값)에서 끝나고, 그 장면의 라벨은 전부 'return ...'이다. 고수준에는 기억이 없으므로 거기가 고정점이다(실물에서도 그랬다. 학습에 쓰지 않은 영상 4개 중 3개도 'return ...'으로 끝났고 끝난 뒤 'lift ...'로 넘어간 적은 없다. 나머지 하나는 'move the plate over ...'로 끝났다). `/autosteer`를 다시 넣는 것으로는 새 바퀴가 시작되지 않는다. 새 바퀴는 철판을 비커에 돌려놓은 상태에서 `/reset`(처음 자세로 3초 복귀, task 복원) → `/start` → `/autosteer`. 두 번째 `/start`에는 플러그인의 진입 램프가 없다(프로세스당 한 번. 클램프와 속도 제한만 걸린다). - 실기 `sync` 루프는 30 Hz가 아니라 14~21 Hz로 재어져 있다(다른 정책으로 잰 값. 틱의 대부분이 카메라·CAN 등 로봇 쪽이다). 시연보다 1.4~2배 느리게 움직이는 것은 고장이 아니다. 그래서 `--duration`은 주지 않았다(대화형에서 시간이 다 되면 홈으로 가지 않고 그 자리에 멈추며 autosteer도 꺼진다). ## 확인한 범위 (2026-09-21, 학습 서버) - 빈 자리에서 `uv sync --frozen --extra train --extra annotate`로 롤아웃 PC의 `.venv`(lerobot 0.6.1)를 재현했다. 그 `lerobot-rollout --help`에는 `--interactive`가 없다(위 오류와 같은 조건). - 그 위에서 1번을 글자 그대로 돌렸다: constraints 143줄(`openarm-control @ git+...` 한 줄 포함), 설치 exit 0, 패치 적용, `--interactive` 있음, `--policy.type`에 `pi052` 있음, lerobot 0.6.2 / torch 2.11.0+cu130 / transformers 5.5.4, 로봇 종류에 `openarm_sciedu`·`openarm_sciedu_sim`. `lerobot[groot]==0.6.1`을 얹어도 기존 버전은 바뀌지 않는다(dry-run). - 그렇게 깐 `.venv_pi052`만으로 3번 명령을 로봇만 시뮬로 바꿔 끝까지 돌렸다(`/start` → `Autosteer on` → 3초마다 생성 → `/stop` → 하강, exit 0). - 토큰 없이는 PaliGemma 토크나이저가 401인 것, 2번 넷째 줄이 16.6 GB만 받는 것(dry-run)을 확인했다. - 롤아웃 PC(RTX 5090, 드라이버 595.84, uv 0.12.3)에서 1·2번이 막힘 없이 끝났다(2026-09-21): `--interactive` 2, `hf auth whoami` = leapshared, 토크나이저와 모델 받아짐, `uv pip check` 통과. 로봇 없이 sync 엔진과 같은 경로로: 액션 청크 평균 오차 0.59도, next_subtask 8/8, 청크 추론 155 ms(첫 회 2.1초), 문장 생성 116 ms(첫 회 1.8초), 로딩 피크 24.2 GiB·추론 중 8.9 GiB. - **실물 첫 실행(2026-09-21, 운영자)**: 로딩·카메라 3대·홈 이동(110도를 6초에, 남은 오차 3도)·sync 엔진은 정상. 로딩 중에 미리 친 `/start`가 두 번 다 배너 직후에 읽혔고, `/autosteer` 목표가 잘려 들어가 목록 밖 문장이 나왔다(위 3번). 목표를 한 줄로 넣은 실행에서는 문장이 순서대로 돌았지만 **팔은 철판 잡기를 놓쳤고, 빈손으로 들어 올리고 다른 손으로 가리킨 뒤 중간 단계를 건너뛰어 'return to the home posture'까지 갔다.** 고수준은 지금 영상과 task 문장만 보고 성공한 시연 200개로만 학습해서 "잡기를 놓쳤다"를 모른다. 과제는 성공하지 못했다. - **확인하지 못한 것**: 리얼센스·CAN이 붙은 상태의 루프 속도(Cadence summary를 아직 받지 못했다), 잡기가 왜 빗나갔는지.