← Back
2026-04-27 · engineering

Notes — MemER prompt-format diff: this repo vs Anthropic MemER (sister repo)

TL;DR. > Phase-0 결과물. 이 비교가 전체 방향(Path A vs Path B)을 결정함.

Phase-0 결과물. 이 비교가 전체 방향(Path A vs Path B)을 결정함.

TL;DR — Path B 확정

이 repo의 Qwen3VLModelMemER (examples/robomme/subgoal_prediction/qwenvl/api_memer.py) 의 prompt 와 sister repo의 memer_eval/contract.py여러 축에서 서로 다름 — 단순 wording 차이가 아니라 (1) 카메라 구성 자체 (단일 front-view vs wrist+ext1 vertical stack), (2) <image> 레이아웃 (인라인 리스트 vs 한 줄당 하나), (3) output schema (grounded <|box_start|>(x,y)<|box_end|> vs plain text), (4) inference engine (ms-swift PtEngine vs HF Transformers) 가 다름.

LoRA Yinpei/vlm_subgoal_predictor / memer/grounded_subgoal/checkpoint-1300이 repo의 ms-swift 템플릿으로 학습됨 (scripts/finetune_vlm_subgoal_predictor.sh 참조). 따라서 sister repo의 MemERDeploymentPolicy 를 LoRA에 그대로 얹으면 학습 분포 밖에서 동작 — 결과는 dusting 체크포인트를 RoboMME 에 돌렸을 때처럼 OOD-스러워질 위험이 큼.

Path B: 이 repo 의 Qwen3VLModelMemER 를 그대로 driver 로 쓰고, 출력 jsonl 만 sister repo 의 renderer (render_robomme_subtask_video.py) 가 기대하는 schema 로 변환해서 dump한다.

핵심 차이 한 줄 요약

이 repo (Yinpei MemER LoRA, ms-swift) Sister repo (Anthropic MemER, HF)
학습 prompt 출처 build_vlm_subgoal_dataset_memer.py memer_eval/contract.py
추론 engine swift.llm.PtEngine (ms-swift) HF transformers + Qwen3VLForConditionalGeneration
카메라 입력 단일 front-view (sim front_rgb_list) wrist + ext1 vertically stacked (make_camera_stack)
학습 시 image size 256×256 320×180 (사용자 default)
시스템 프롬프트 키워드 "input images from the front-view camera" "video input from the egocentric camera"
user prompt 도입 The task goal is: {task_goal} Task: {instruction}
메모리 도입 wording "...selected frames from the entirety of the full execution..." "...from the entirety of the full video..."
recent context wording "Here is current input image list from the front-view camera: [...]" "Here is a video of the most recent actions the robot has executed:" + N×<image>
<image> 레이아웃 인라인 리스트 [<image>, <image>, ...] (memory + recent 각 그룹별 한 묶음) 한 줄에 한 개씩
마지막 질문 "What subtask should the robot execute and what is the keyframe position?" (있음) (없음)
video demo prefix "The task has a video input for initial setup: <video>\n" (옵션) (없음 — 전부 image 기반)
Output schema (grounded) {"current_subtask": "...<\|box_start\|>(x,y)<\|box_end\|>...", "keyframe_positions": [int, ...]} {"current_subtask": "...", "keyframe_positions": [int, ...]} (plain text, no box tokens)
keyframe_positions 의미 현재 호출에서 보낸 execution frame list 의 1-indexed 위치 동일 (1-indexed in recent video)
memory clustering merge_key_frame_paths(dist=8) — 정렬 후 dist≤8 그룹화, 그룹별 g[len(g)//2] (upper median) cluster_candidate_indices(merge_distance=5) — 동일 알고리즘, median_low, FIFO memory_length 캡 (default 8)
recent frames 샘플링 마지막 8 프레임을 stride 2 로 (idx -1, -3, -5, ..., -15) 마지막 N 프레임을 frame_subsample (=5) 단위로
sampling RequestConfig(max_tokens=128, temperature=0) max_new_tokens=128, greedy default
attn impl flash_attention_2 (코드 상) sdpa (sister repo의 working setup)

두 repo 의 prompt 실제 모양

이 repo (api_memer.py:201-225)

SYSTEM:
You are a robot program that predicts actions. The current input images
from the front-view camera shows the most recent actions the robot has
executed. The past keyframes are selected frames of particular importance
from all the actions the robot has executed so far. Based on these,
output the current subtask the robot should execute and nothing else.
Some tasks may have a video input for initial setup, some may not.

Return a JSON with:
- current_subtask: the action that should be executed at the current timestep
- keyframe_positions: list of frame positions (1-indexed) from the current
  input images where actions change

USER:
[The task has a video input for initial setup: <video>\n]   # 옵션
The task goal is: <task_goal>
Here are the selected frames from the entirety of the full execution that
are of particular importance:[<image>, <image>, ..., <image>]
Here is current input image list from the front-view camera:
[<image>, <image>, ..., <image>]

What subtask should the robot execute and what is the keyframe position?

ms-swift 가 이 메시지를 받아 Qwen3-VL chat template 을 적용. <image> placeholder 는 images=[...] 리스트와 1:1 매칭되며 순서는 key_frame_paths + current_execution_frame_paths 임 (api_memer.py:211).

Sister repo (contract.py:11-41)

SYSTEM:
You are a robot program that predicts actions.
The video input from the egocentric camera shows the most recent actions
the robot has executed. The images are selected frames of particular
importance from all the actions the robot has executed so far. Based on
these, output the current subtask the robot should execute and nothing else.

Return a JSON with:
- current_subtask: the action that should be executed at the current timestep
- keyframe_positions: list of frame positions (1-indexed) from the video
  input where actions change

USER:
Task: <instruction>
Here are the selected frames from the entirety of the full video that are
of particular importance:
<image>
<image>
...
Here is a video of the most recent actions the robot has executed:
<image>
<image>
...

Camera 매핑 (training H5 → eval LeRobot v2)

학습 dataset builder 는 H5 의 episode_data["timestep_{i}"]["obs"]["front_rgb"] 하나만 씀 (build_vlm_subgoal_dataset_memer.py:358,391). front_rgb_list 는 sim env 의 obs key (examples/robomme/env_runner.py:62) 로 RoboMME 의 정면 외부 카메라.

LeRobot v2 변환본은 3개 카메라를 갖고 있음:

videos/chunk-000/
  observation.images.exterior_image_1_left
  observation.images.exterior_image_2_left
  observation.images.wrist_image_left

RoboMME H5 → LeRobot 변환에서 front_rgb 는 (이름과 sister repo의 dusting 실험에서 사용한 카메라 정렬을 종합하면) exterior_image_1_left 에 해당함. ext_2 는 보조 시점.

inference 시 디코드해야 할 mp4: exterior_image_1_left/episode_000000.mp4 한 개만. wrist + ext1 stack 을 보내면 학습 분포와 다름.

Memory 메커니즘 비교

둘 다 "1D 정렬 후 인접한 candidate index 들을 한 클러스터로 묶고 median 을 대표값으로 뽑는다" 는 점은 동일. 차이점:

→ Path B 에서는 이 repo 의 mechanism 그대로 사용 (LoRA 가 학습된 상태에 맞음). renderer 가 기대하는 memory_indices_after 필드는 self.key_frame_paths.keys() 의 정렬된 리스트로 그대로 채울 수 있음.

Output schema (Path B 출력 jsonl)

Sister repo 의 render_robomme_subtask_video.py 가 한 줄당 읽는 키:

{
  "timestep": <int>,
  "instruction": "<task instruction>",
  "predicted_subtask": "<parsed subtask string, no box tokens>",
  "predicted_keyframe_positions": [<int>, ...],
  "mapped_keyframe_indices": [<int>, ...],
  "memory_indices_after": [<int>, ...]
}

이 repo 의 Qwen3VLModelMemER.call() 에서 추출 가능한 정보:

이 매핑은 driver 가 Qwen3VLModelMemER 를 wrap 할 때 export 만 추가하면 끝.

Path B 실행 계획 (Phase 2 에서 구현할 driver 골자)

  1. scripts/eval_robomme_memer_lora.py 신설.
  2. Qwen3VLModelMemER(adapter_path=...) 인스턴스 생성.
  3. LeRobot v2 의 videos/chunk-000/observation.images.exterior_image_1_left/episode_000000.mp4 디코드 → numpy frame list.
  4. start_new_episode(save_dir, video_query=None, task_goal=...). 여기서 task_goalmeta/episodes.jsonltasks[0] 사용.
  5. 매 raw frame t 마다 add_execution_frame(frame[t]). 호출 빈도는 sister repo 와 같이 stride=5 마다 한 번 call() 을 트리거.
  6. call() 결과(string) + Qwen3VLModelMemER 의 내부 상태( current_execution_frame_paths, key_frame_paths) 를 가져와서 위 schema 로 한 줄을 jsonl 에 append.
  7. run_info.json 도 sister repo 와 동일 schema 로 dump (renderer 는 안 쓰지만 사후 비교용).

이 구조는 sister repo 의 eval_robomme_subtask.py 와 거의 동일한 shell 을 갖되, policy.step(...) 호출만 이 repo 의 Qwen3VLModelMemER 호출로 바꾼 것.