Phase-0 결과물. 이 비교가 전체 방향(Path A vs 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) |
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).
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>
...
학습 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 을 보내면 학습 분포와 다름.
둘 다 "1D 정렬 후 인접한 candidate index 들을 한 클러스터로 묶고 median 을 대표값으로 뽑는다" 는 점은 동일. 차이점:
api_memer.py:135-157): cluster 거리 임계 dist=8,
upper median (g[len(g)//2]), FIFO 길이 제한은 명시적이지 않고
merge_key_frame_paths 가 매 호출마다 전체 dict 를 재계산함. dict
자체는 {key_frame_id: image_path} 형태로 유지되고, 새 keyframe 이
들어오면 update_history_subgoals 가 추가 후 merge.memer_eval/memory.py:21-89): cluster 거리 기본
merge_distance=5, median_low, FIFO memory_length 로 마지막 N
개만 노출 (selected_indices() 가 [-memory_length:]).→ Path B 에서는 이 repo 의 mechanism 그대로 사용 (LoRA 가 학습된
상태에 맞음). renderer 가 기대하는 memory_indices_after 필드는
self.key_frame_paths.keys() 의 정렬된 리스트로 그대로 채울 수 있음.
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() 에서 추출 가능한 정보:
timestep ← 우리가 driver level 에서 들고 있는 raw frame counterinstruction ← task_goal (api_memer.py:90)predicted_subtask ← _parse_subgoal_for_vla(response) (box 토큰 →
scaled coords 로 치환된 plain text). 이게 sister repo 의
predicted_subtask 와 가장 가까운 형태.predicted_keyframe_positions ← json.loads(response)["keyframe_positions"]mapped_keyframe_indices ← keyframe_positions 각 1-indexed 위치를
current_execution_frame_paths 의 path 에서 step_(\d+)_image.png
추출한 raw frame index 로 변환 (이미 update_history_subgoals 안에
비슷한 로직 있음 — int_idx).memory_indices_after ← sorted(self.key_frame_paths.keys()) (merge
적용 후 — 즉 update_history_subgoals 호출 직후 시점)이 매핑은 driver 가 Qwen3VLModelMemER 를 wrap 할 때 export 만 추가하면
끝.
scripts/eval_robomme_memer_lora.py 신설.Qwen3VLModelMemER(adapter_path=...) 인스턴스 생성.videos/chunk-000/observation.images.exterior_image_1_left/episode_000000.mp4
디코드 → numpy frame list.start_new_episode(save_dir, video_query=None, task_goal=...). 여기서
task_goal 은 meta/episodes.jsonl 의 tasks[0] 사용.t 마다 add_execution_frame(frame[t]). 호출 빈도는
sister repo 와 같이 stride=5 마다 한 번 call() 을 트리거.call() 결과(string) + Qwen3VLModelMemER 의 내부 상태(
current_execution_frame_paths, key_frame_paths) 를 가져와서
위 schema 로 한 줄을 jsonl 에 append.run_info.json 도 sister repo 와 동일 schema 로 dump (renderer 는
안 쓰지만 사후 비교용).이 구조는 sister repo 의 eval_robomme_subtask.py 와 거의 동일한
shell 을 갖되, policy.step(...) 호출만 이 repo 의 Qwen3VLModelMemER
호출로 바꾼 것.