← Back
2026-04-15 · research

MemER 공식 레포 공개 범위 & 재현 가능성 감사

TL;DR. 논문 ["MemER: Scaling Up Memory for Robot Control via Experience Retrieval"](https://arxiv.org/abs/2510.20328) (ICLR 2026, Sridhar et al.) 의 공식 레포를 받아왔다. 직접 돌려보거나 본인 태스크에 붙이기 전에 (1) 어디까지 코드가 공개돼 있고, (2)…

배경/목적 (왜)

논문 "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


1. 디렉토리 구조 (전체 스냅샷)

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회 등장.


2. 공개되어 있는 것 (✅)

2.1 데이터 변환 파이프라인 — scripts/generate_sft_data.py

LeRobot 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.

2.2 논문 3개 태스크의 keyframe rule JSON — configs/tasks_from_paper/

2.3 고수준 정책 배포/추론 — 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).

2.4 외부 호스팅 아티팩트 (HF Hub)

2.5 학습 레시피 문서 — 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__.pyMEMER_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 환경변수.


3. 공개되지 않은 것 (❌)

  1. 학습 코드 자체. 이 레포엔 없음. 공식 QwenLM/Qwen3-VL 레포를 clone하고 직접 dataset entry를 등록하는 간접 방식. 즉 학습을 돌리려면 외부 레포 상태에 의존.
  2. Low-level policy (VLA). 코드/체크포인트 전혀 없음. README는 openpi 권장, GR00T/SmolVLA도 가능하다고만 언급. 통합은 pseudo-code 수준 (low_level_policy.act(obs, language_command=...)).
  3. 시뮬레이터/실로봇 환경 어댑터. env.reset(), env.step(), 카메라 캡처, action 실행 루프 전부 placeholder. mujoco/robosuite/gym 어떤 것도 import되지 않음 (Grep: sim|mujoco|gym|robosuite|env\.step → README.md 1개 파일만 매치, 실제 구현 없음).
  4. Dusting 외 태스크의 데이터/체크포인트. object_search, counting은 keyframe rule JSON만 존재. 공개 데이터셋/모델 없음 → 실질 end-to-end 재현은 dusting 하나.
  5. 논문 전체 벤치마크 재현. 비교 baseline, ablation, 여러 태스크 동시 평가 스크립트 없음. 오직 "offline subtask accuracy" 한 축만.
  6. 로봇 config. configs/robots/에 Franka Panda stacked RGB 하나뿐. 다른 로봇은 본인이 추가.
  7. 테스트/CI. tests/ 없음, .github/ 없음.
  8. torch. pyproject.toml에서 의도적으로 빼놔서 CUDA 버전에 맞는 wheel을 사용자가 직접 설치해야 함. 기본 의존은 numpy==2.2.6, Pillow, lerobot==0.4.4, tqdm 만. eval extra로 transformers>=4.57.0, qwen-vl-utils 추가.

4. 직접 돌려보기 — 시나리오별 가능성

시나리오 A: 오프라인 롤아웃 평가 (dusting) — ⭐ recommended, 바로 가능

레포가 공식적으로 지원하는 유일한 완전 재현 경로. 시뮬/로봇 불필요.

필요한 것: 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 내부 파일을 먼저 확인할 것.

시나리오 B: 본인 데이터셋으로 오프라인 평가 — 가능 (작업 필요)

LeRobot v3 + subtask 라벨 (meta/subtasks.parquet) 이 붙어있으면 그대로 돌릴 수 있음. - 카메라 키만 맞춰서 configs/robots/에 새 layout JSON 만들거나 --camera-keys로 직접 지정. - instruction은 --high-level-instruction으로 override 가능. - subtask 라벨 없는 dataset은 먼저 LeRobot subtask guide로 annotation 붙여야 함.

시나리오 C: 본인 데이터셋으로 직접 파인튜닝 — 가능 (외부 레포 필요)

  1. scripts/generate_sft_data.py 로 SFT JSON 생성.
  2. TRAINING_QWEN3VL.md 레시피대로 QwenLM/Qwen3-VL 레포 clone + dataset entry 수동 등록.
  3. 논문 설정은 dusting 50-demo 기준 2×B200, 1500 steps. 본인 데이터 크기에 맞춰 step/lr 조정 필요.

시나리오 D: 시뮬레이터 온라인 rollout — 레포 밖 작업 필요

레포에 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. 논문 수준 재현을 노린다면 이 경로는 공수가 큼.

시나리오 E: 실로봇 온라인 rollout — 레포 밖 작업 필요


5. VLM → VLA 명령 전달이 코드에서 어떻게 보이는가

실제 코드로 볼 수 있는 것 ✅

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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 같은 건 없음.

코드엔 없는 것 ❌

  1. VLA 쪽 구현: low_level_policy.act(...) 는 README pseudo-code에만 존재. openpi 연결 glue, action decoding 없음.
  2. 주파수/sync 로직: high-level과 low-level이 다른 주파수로 도는 게 일반적이지만, 그 sync 코드는 없음. frame_subsample=5 가 유일한 힌트 — low-level freq / high-level freq 비율로 해석하면 되지만, 실제 스케줄링은 사용자 몫.
  3. 문자열 외 신호 전달: 없음. 정말로 subtask text 한 개만 넘기는 단순한 구조라는 게 코드로 확정.

구조 한 줄 요약

"VLM이 task-level planner처럼 현재 수행할 단일 subtask 문자열을 뽑고, VLA는 그 문자열을 기존 language-conditioned policy의 instruction 슬롯에 꽂아 동작한다. VLM의 memory는 VLM 자신의 과거 예측 keyframe을 클러스터링해서 유지되며, low-level은 이 메모리를 전혀 보지 않는다."


결과 (수치)

의미 (Takeaway)

보완점/다음 (Next)