논문 "MemER: Scaling Up Memory for Robot Control via Experience Retrieval" (ICLR 2026, Sridhar et al.) 의 공식 레포를 받아왔다. 직접 돌려보거나 본인 태스크에 붙이기 전에 (1) 어디까지 코드가 공개돼 있고, (2) 무엇이 외부 의존이며, (3) VLM → VLA 명령 전달이 실제 코드로 어떻게 구현돼 있는지 를 확정해두어야 함. 논문만 보고 들어가면 재현 시도 후에야 "시뮬/로봇 환경이 없네", "학습 스크립트가 없네" 같은 것들을 발견하게 되어 시간 낭비가 크다.
레포 경로: /home/nas_main/taewoongkang/repos/Robotics/memer
HEAD: fb6dbd0 initial code release
memer/
├── README.md (319 lines, 메인 문서)
├── TRAINING_QWEN3VL.md (77 lines, Qwen3-VL 파인튜닝 레시피)
├── pyproject.toml (uv 기반, 매우 얇음)
├── .gitignore (pycache / uv.lock만 무시)
├── configs/
│ ├── robots/
│ │ └── panda_stacked_rgb_camera_layout.json (Franka Panda 하나뿐)
│ └── tasks_from_paper/
│ ├── dusting_keyframe_rules.json
│ ├── object_search_keyframe_rules.json
│ └── counting_keyframe_rules.json
├── memer_eval/ (배포/평가용 Python 패키지)
│ ├── __init__.py (공개 API surface)
│ ├── camera_layout.py (카메라 레이아웃 config 로더)
│ ├── contract.py (시스템/유저 프롬프트 + JSON 출력 contract)
│ ├── dataset_metadata.py (LeRobot 메타데이터 헬퍼)
│ ├── deploy.py (MemERDeploymentPolicy — 온라인 stateful wrapper)
│ ├── inference.py (QwenStructuredPredictor — HF 로딩 + JSON 파싱)
│ ├── memory.py (EpisodicMemory — 1D 클러스터링 기반 키프레임 메모리)
│ ├── rollout.py (오프라인 롤아웃 평가 엔진)
│ └── utils.py (align_frames_with_subsampling 하나만)
└── scripts/
├── generate_sft_data.py (LeRobot → Qwen SFT JSON + media/, 910 lines)
└── eval_subtask_rollout.py (rollout.py CLI, 148 lines)
테스트 디렉토리 없음. CI 없음. sim/gym/mujoco/robosuite/env.step 키워드는 README의 pseudo-code에만 1회 등장.
scripts/generate_sft_data.pyLeRobot v3 subtask-labeled 데이터셋을 Qwen3-VL SFT 포맷으로 변환하는 완전한 파이프라인.
- 출력: output_dir/train.json + output_dir/media/episode_XXXXXX/frame_XXXXXX.jpg
- Rule-based keyframe selection: regex/exact match + first/last/both/none select mode. 논문의 "어떤 subtask에서 메모리 키프레임을 뽑을 지" 규칙을 JSON으로 표현.
- Subsample + rolling window: --frame_subsample, --recent_frames_length, --keyframes_length, --prediction_horizon 전부 CLI 플래그. recent context window는 FIFO, 메모리는 rule로 뽑힌 과거 프레임.
- 멀티 카메라 stacking: 여러 카메라 뷰를 세로로 concat + per-view resize (view_width × view_height × num_cams).
- 병렬화: ProcessPoolExecutor 에피소드 단위. --num_workers 20 기본.
- LeRobot codebase_version 체크: v3.x 아니면 경고 또는 fail.
configs/tasks_from_paper/dusting_keyframe_rules.json: "take ... off shelf", "pick up duster", "dust ... shelf", "place ... on ... shelf" 패턴을 last로, "reset duster"/"put down duster"는 none.object_search_keyframe_rules.json, counting_keyframe_rules.json: 마찬가지로 패턴 제공. 단, 해당 데이터셋/체크포인트는 공개 X.memer_eval/contract.py — 프롬프트/출력 contract (논문 그대로)
- DEFAULT_SYSTEM_PROMPT: "You are a robot program that predicts actions. ... Return a JSON with: current_subtask, keyframe_positions".
- build_user_prompt(instruction, memory_count, recent_count): Task: {instruction} + memory 이미지 블록 + recent 비디오 블록. <image> placeholder를 쓴 뒤 build_user_message에서 실제 PIL 이미지로 interleave.
- 출력 키 alias: current_subtask 또는 current_primitive 둘 다 파싱 허용.
memory.py — EpisodicMemory (Experience Retrieval의 실체)
- cluster_candidate_indices(indices, merge_distance): 정렬된 1D 후보 프레임 인덱스를 merge_distance 이하 간격으로 묶고, 각 클러스터에서 median_low를 representative로 채택.
- EpisodicMemory.add_candidates: VLM이 뱉은 candidate keyframe들을 계속 누적.
- selected_indices(): 현재까지의 클러스터 representative들 중 최근 memory_length개 반환 (FIFO).
- visible_indices(current_context_indices): recent context에 이미 있는 프레임은 제외하고 visible memory만 반환 (중복 방지).
deploy.py — MemERDeploymentPolicy (온라인 stateful wrapper)
- from_qwen_checkpoint(model_path, ...): Qwen3-VL 체크포인트 로드 → wrapper 생성.
- reset(instruction): 프레임 버퍼 clear + EpisodicMemory re-init.
- step(*observations, instruction=None) -> DeploymentStepResult: 한 개 또는 여러 observation을 받고, 마지막 timestep에서만 한 번 inference. 즉 청크 단위로 받아도 stateful하게 유지됨.
- observation 타입: single camera면 raw image 직접, multi-camera면 {camera_key: image} dict. PIL/numpy/torch/LeRobot dict-style 다 지원.
- 매 step의 흐름:
1. 새 observation을 내부 _frames 리스트에 append.
2. 현재 timestep 기준 context_indices 계산 (recent FIFO).
3. memory.visible_indices(context_indices) → 프롬프트에 들어갈 메모리 키프레임.
4. 이미지 리스트 = memory_indices_before + context_indices의 _frames[i]들.
5. predictor.predict(prompt, images) → ModelPrediction(current_subtask, keyframe_positions, parse_ok, ...).
6. 예측된 keyframe 상대 위치를 절대 프레임 인덱스로 매핑하고 memory.add_candidates.
- DeploymentStepResult: 예측된 subtask, parse 성공 여부, predicted/mapped/invalid keyframe positions, context/memory indices, raw text 전부 노출.
inference.py — QwenStructuredPredictor
- AutoModelForImageTextToText + AutoProcessor 로 Qwen3-VL 로드. trust_remote_code=True.
- attn backend: 기본은 cuda면 flash_attention_2, 아니면 sdpa. CLI로 override 가능.
- qwen_vl_utils.process_vision_info로 image/video 전처리.
- predict(prompt, images): chat template 적용 → processor → model.generate(do_sample=False, max_new_tokens=max_new_tokens) → batch_decode → parse_prediction_text.
- parse_prediction_text: strip → fenced code block 분해 → balanced {} 추출 → json.loads 시도. current_subtask / current_primitive alias 둘 다 허용. keyframe_positions는 list[int] 강제 (float은 정수일 때만, bool 거부, 음수 문자열도 허용).
rollout.py — 오프라인 평가 엔진
- RolloutConfig dataclass로 모든 플래그 캡슐화.
- load_dataset_context: LeRobotDatasetMetadata로 tasks/subtasks parquet 읽어서 task_map, subtask_map dict 구축. camera_keys는 metadata → fallback으로 info["features"]에서 image/video dtype 탐색.
- PREFERRED_STACKED_CAMERA_KEYS = ("observation.images.wrist_left", "observation.images.exterior_1_left").
- EpisodeFrameCache: lazy in-memory 프레임 렌더러. multi-camera면 stack_frame_images + resize, single이면 raw array → PIL.
- evaluate_rollout 루프:
- 에피소드별로 LeRobotDataset을 다시 열어 items 수집.
- timestep 0..N-1 에서 context_indices = build_recent_context_indices(timestep, subsample, recent_len).
- target_index = min(timestep + prediction_horizon * frame_subsample, total-1) — 논문과 동일한 future label shift.
- raw_correct = parse 성공 && 예측 문자열 == target label 그대로, normalized_correct = whitespace normalize 후 비교.
- parse 실패는 별도 카운트, memory 업데이트 안 함.
- 출력: summary.json (overall 메트릭 + 전체 config + runtime + dataset 정보), episodes.json (에피소드별 정확도), label_metrics.json (subtask별 support/raw_acc/normalized_acc), predictions.jsonl (timestep별 한 줄; --save-raw-responses 주면 raw text 포함).
camera_layout.py: JSON config → CameraLayout(camera_keys, view_width, view_height, notes). 양수 validation.
dataset_metadata.py: build_index_mapping_from_dataframe (subtasks parquet → {index: label}), extract_instruction (override 우선, 없으면 task_map, 없으면 fallback 문자열), extract_subtask_label (label 없으면 에러).
utils.py: align_frames_with_subsampling 하나. 키프레임 절대 인덱스를 subsample grid에 align (floor division).
ajaysri/memer-dusting-qwen3vl-4b-step-1500 — Qwen3-VL-4B-Instruct 기반 dusting finetune, 1500 steps.ajaysri/dusting_train_50_v3_subtasks_rgb (50 demos).ajaysri/dusting_test_10_v3_subtasks_rgb (10 episodes, 6162 timesteps).TRAINING_QWEN3VL.md코드는 없고 명령어만. 흐름:
1. git clone https://github.com/QwenLM/Qwen3-VL.git && cd qwen-vl-finetune && pip install -e ../qwen-vl-utils
2. Qwen 레포의 qwenvl/data/__init__.py에 MEMER_SFT = {"annotation_path": ".../train.json", "data_path": "..."} 수동 등록.
3. 예시 커맨드: 2× B200, --nproc_per_node=2, lr 6e-5, bs 8 × grad_accum 8, 1500 steps, max_pixels 115200, min_pixels 50176, warmup_ratio 0.05, cosine, bf16, gradient checkpointing, vision/mlp frozen + LLM만 학습 (tune_mm_vision=False, tune_mm_mlp=False, tune_mm_llm=True).
4. QWEN_VL_ATTN_IMPL=flash_attention_2 환경변수.
QwenLM/Qwen3-VL 레포를 clone하고 직접 dataset entry를 등록하는 간접 방식. 즉 학습을 돌리려면 외부 레포 상태에 의존.low_level_policy.act(obs, language_command=...)).env.reset(), env.step(), 카메라 캡처, action 실행 루프 전부 placeholder. mujoco/robosuite/gym 어떤 것도 import되지 않음 (Grep: sim|mujoco|gym|robosuite|env\.step → README.md 1개 파일만 매치, 실제 구현 없음).configs/robots/에 Franka Panda stacked RGB 하나뿐. 다른 로봇은 본인이 추가.tests/ 없음, .github/ 없음.pyproject.toml에서 의도적으로 빼놔서 CUDA 버전에 맞는 wheel을 사용자가 직접 설치해야 함. 기본 의존은 numpy==2.2.6, Pillow, lerobot==0.4.4, tqdm 만. eval extra로 transformers>=4.57.0, qwen-vl-utils 추가.레포가 공식적으로 지원하는 유일한 완전 재현 경로. 시뮬/로봇 불필요.
필요한 것:
1. GPU + CUDA 호환 torch (Qwen3-VL-4B bf16 ~9GB VRAM).
2. uv sync --extra eval (torch 선설치 후).
3. huggingface-cli download ajaysri/memer-dusting-qwen3vl-4b-step-1500 --local-dir ./ckpts/checkpoint-1500.
4. 평가 데이터셋은 LeRobot이 HF cache로 자동 다운로드 (--repo-id ajaysri/dusting_test_10_v3_subtasks_rgb).
5. (선택) ffmpeg — 비디오 렌더링용.
커맨드 (README 예시 그대로):
python3 scripts/eval_subtask_rollout.py \
--model-path ./ckpts/checkpoint-1500 \
--processor-path /path/to/Qwen3-VL-4B-Instruct \
--lerobot-path ./data/dusting_test_10_v3_subtasks_rgb \
--repo-id ajaysri/dusting_test_10_v3_subtasks_rgb \
--output-dir ./eval_outputs/dusting_test_rollout \
--high-level-instruction "What subtask should the robot execute to remove the items from the shelves, dust the shelves, and place the items back on the shelves?" \
--camera-layout-config configs/robots/panda_stacked_rgb_camera_layout.json \
--frame-subsample 5 --recent-frames-length 8 --memory-length 8 \
--prediction-horizon 2 --merge-distance 5 \
--attn-implementation sdpa --save-raw-responses \
--max-episodes 1 # 전체 돌리려면 이 줄 제거
기대 결과: 논문의 94.21% subtask accuracy 재현. 평가 산출물 4개 JSON/JSONL.
주의: --processor-path는 체크포인트에 processor 파일이 포함돼 있지 않을 가능성이 있어서 원본 Qwen/Qwen3-VL-4B-Instruct processor를 별도로 받아 지정하는 걸 README가 예시로 보여줌. 체크포인트 zip 내부 파일을 먼저 확인할 것.
LeRobot v3 + subtask 라벨 (meta/subtasks.parquet) 이 붙어있으면 그대로 돌릴 수 있음.
- 카메라 키만 맞춰서 configs/robots/에 새 layout JSON 만들거나 --camera-keys로 직접 지정.
- instruction은 --high-level-instruction으로 override 가능.
- subtask 라벨 없는 dataset은 먼저 LeRobot subtask guide로 annotation 붙여야 함.
scripts/generate_sft_data.py 로 SFT JSON 생성.TRAINING_QWEN3VL.md 레시피대로 QwenLM/Qwen3-VL 레포 clone + dataset entry 수동 등록.레포에 sim 어댑터가 없기 때문에 직접 구축 필요:
- Franka 기반 sim (robosuite/mimicgen/robocasa 등) 에서 카메라 키 이름을 observation.images.wrist_left, observation.images.exterior_1_left로 매핑하는 shim 작성.
- MemERDeploymentPolicy 는 wrapper 그대로 재사용 가능 (step(dict) 호출).
- Low-level policy는 본인이 해당 sim 환경에서 subtask-labeled 데이터로 따로 학습. openpi 파인튜닝이 가장 현실적.
- 완전 DIY. 논문 수준 재현을 노린다면 이 경로는 공수가 큼.
MemERDeploymentPolicy.step(dict) 호출.VLM 입력 contract (contract.py):
- 시스템 프롬프트: "robot program that predicts actions. video input = 최근 행동, selected frames = 과거 중요 프레임. current_subtask + keyframe_positions JSON으로 답해라."
- 유저 프롬프트: Task: {instruction}\n[memory block with <image>×N]\n[recent video block with <image>×M].
- 이미지는 chat message 안에 placeholder 순서대로 interleave.
Experience Retrieval 메모리 동작 (memory.py):
- VLM이 매 step에서 "최근 window에서 중요한 프레임 위치"(1-indexed)를 뱉음 → 절대 프레임 인덱스로 매핑 → EpisodicMemory에 candidate로 누적.
- 1D 클러스터링 (merge_distance = subsampled merge × frame_subsample 로 raw frame 단위 변환) → median_low가 representative.
- 다음 step에서 visible_indices(context_indices) 로 recent에 없는 memory rep만 골라 프롬프트에 넣음.
- 이게 논문의 "experience retrieval"의 실체: 과거 VLM 자신의 예측을 클러스터링해서 stable keyframe pool을 만들고 FIFO로 maintain.
Stateful step 루프 (deploy.py):
- MemERDeploymentPolicy.step() 하나가 전체 contract를 캡슐화.
- 매 호출: frame 추가 → context_indices/memory_indices_before 계산 → prompt 빌드 → predictor.predict → predicted keyframes를 memory에 fold → DeploymentStepResult 반환.
- 청크 observation (step(obs1, obs2, obs3)) 도 지원 — 모두 버퍼링하고 마지막 timestep에서만 inference.
JSON 파싱 robustness (inference.py:parse_prediction_text):
- fenced code block, balanced {}, alias key, bool 거부, float-to-int 강제 전부 처리.
- parse 실패 시 parse_ok=False + parse_error 노출, memory 업데이트 skip.
VLA로 전달되는 실체: DeploymentStepResult.current_subtask 문자열 하나. 예: "pick up duster", "dust top shelf". 이게 통합 포인트의 전부.
python
result = high.step({"observation.images.wrist_left": w, "observation.images.exterior_1_left": e})
action = low_level_policy.act(obs, language_command=result.current_subtask)
즉 plain-text subtask label 한 개가 VLM → VLA 브리지. 임베딩 hand-off나 shared latent 같은 건 없음.
low_level_policy.act(...) 는 README pseudo-code에만 존재. openpi 연결 glue, action decoding 없음.frame_subsample=5 가 유일한 힌트 — low-level freq / high-level freq 비율로 해석하면 되지만, 실제 스케줄링은 사용자 몫."VLM이 task-level planner처럼 현재 수행할 단일 subtask 문자열을 뽑고, VLA는 그 문자열을 기존 language-conditioned policy의 instruction 슬롯에 꽂아 동작한다. VLM의 memory는 VLM 자신의 과거 예측 keyframe을 클러스터링해서 유지되며, low-level은 이 메모리를 전혀 보지 않는다."
memer_eval/ 9 + scripts/ 2), config JSON 4개, 문서 2개 (README.md 319 lines, TRAINING_QWEN3VL.md 77 lines), pyproject.toml.current_subtask).memory.py 90줄로 구현. 논문의 기여가 "복잡한 retrieval 메커니즘"이 아니라 "VLM 자체 예측을 축적해서 메모리 키프레임으로 재사용"이라는 관점 자체임이 코드로 명확해진다.ajaysri/memer-dusting-qwen3vl-4b-step-1500 이 processor를 포함하는지 먼저 HF 페이지에서 확인. 없으면 Qwen/Qwen3-VL-4B-Instruct 에서 processor 별도 다운로드 필요. (README 예시는 --processor-path를 원본 model path로 분리해서 줌).transformers>=4.57.0 + AutoModelForImageTextToText API 사용. 현재 로컬 환경에서 호환 torch wheel 선설치 필요.--max-episodes 1 로 먼저 돌려보고 94% 선이 나오는지 확인. 성공하면 전체 10 episode로 확장.ajaysri/ 유저 페이지에서 object_search_*, counting_* 이름으로 공개 dataset 있는지 확인. 없으면 저자에게 요청하거나 dusting만 사용.inference.py 만 교체하면 됨.deploy.py 의 step() return 에 hidden state를 추가하는 식.