260430/research-pi_mem_text_summary_adaptation.md
+ 260430/plan-api_mem_implementation.md Phase 2.
PI Mem 의 long-term language summary mt 를 zero-shot LetSur (Gemini) API 로
구현해서 pi0.5 closed-loop 에 꽂은 첫 BinFill 50 ep. 비교 baseline 은 MemER LoRA
(exp-closed_loop_full_sweep.md) 의 BinFill 64%.
gemini-2.5-flash-lite via LetSur (https://gateway.letsur.ai/v1)flash 시도에서 발견한 truncation 회피thinking_config 은 LetSur 가 무시하는 게 확정 (아래 분석)runs/ckpts/mme_vla_suite/symbolic-grounded-subgoal/79999 (기존)examples/robomme/subgoal_prediction/api_mem/prompts/base.py
의 5-rule + scratchpad+memory_summary+subgoal JSONgemini/prompts/BinFill.py 의 subgoals_grounded /
example_grounded / notes 그대로 import (변경 없음)--args.only-ep0 잘못된 위치 → wrapper level flag 인 --only-ep0 로 수정.
wrapper 의 prepopulate-progress 는 여전히 잘못된 dir(oracle/) 에 씀 → 결국
only-ep0 가 무력화. 이번엔 50 ep full sweep 으로 결정 (B 옵션).gemini-2.5-flash thinking 으로 truncation — 응답이 평균 39 token 만에 끊김
(정상 응답 평균 62 token). extra_body.thinking_config.thinking_budget=0 무시됨.
→ flash-lite 로 교체.examples/robomme/subgoal_prediction/api_mem/api.py default model →
gemini-2.5-flash-lite. extra_body 의 thinking_config 제거.scripts/run_api_mem_one_episode.sh MAX_CLIENT_RESTARTS=10 + restart loop.runs/api_mem_smoke/BinFill/symbolic-grounded-subgoal/ckpt79999/seed7/api_mem/| 측정 | flash (thinking) | flash-lite (non-thinking) |
|---|---|---|
| Parse fails | 41/389 calls (10.5%) | 0/~1100 calls (0%) |
event=error (parse abort 후) |
16/27 = 59% | 0/50 = 0% |
| Truncation 평균 길이 | 39 token (max_tokens 1024 의 4%) | n/a |
| 정상 응답 평균 | 62 token | 비슷 |
| Latency (warm) | 2.5–4.7 s | 비슷 |
→ Truncation 원인은 LetSur 가 extra_body.thinking_config 을 drop, Gemini flash 가
hidden thinking 으로 1024 token 을 거의 다 써버리고 visible content 39 token 만 출력.
flash-lite 은 default non-thinking 이라 이 class 의 failure 가 사라짐.
example 의 좌표 (BinFill.py): <356,499>, <472,700>, <156,483>, <180,376>.
| ep | task_goal | 좌표 source 패턴 |
|---|---|---|
| 1 | put one green cube into bin, press button | call 0~10: parrot (<356,499>, <472,700>). call 15~22: fresh <400,175>. |
| 40 | put two green cubes into bin, press button | call 0~20: fresh (<375,156>, <375,475>, <156,156>, <372,172>). |
| 41 | put one green cube into bin, press button | call 0: fresh <375,175>. call 5: parrot <180,376> (button — 실제로 안 움직이는 위치). |
→ 모델이 ground 가능은 함. 다만 default 는 example 좌표 parroting. 47 fail 의 대다수도 같은 메커니즘일 가능성 높음 — example 좌표가 cube 위치와 우연히 일치하는 ep 만 성공 (ep1, 41). ep40 만 진짜 grounding success.
"The example coordinates above (e.g. <356, 499>) are PLACEHOLDERS to show the JSON shape and grounding format. Do NOT copy them. Re-ground every coordinate from the actual current image. Different episodes have different object layouts."
<y_red, x_red>)
로 교체.../260430/research-pi_mem_text_summary_adaptation.md../260430/plan-api_mem_implementation.md../260430/exp-closed_loop_full_sweep.md (MemER LoRA 64% on BinFill)examples/robomme/subgoal_prediction/api_mem/{api,prompts/base,prompts/__init__}.pyexamples/robomme/subgoal_predictor.py (ApiMemSubgoalPredictor 추가)examples/robomme/eval.py (use_api_mem flag 추가)scripts/run_api_mem_one_episode.sh (50 ep + restart loop)runs/api_mem_smoke/BinFill/symbolic-grounded-subgoal/ckpt79999/seed7/api_mem/examples/robomme/subgoal_prediction/api_mem/prompts/base.py 의 GROUNDED_COORD_INFO
에 명시적 anti-parroting 블록 추가:"CRITICAL: The numeric coordinates in the example episode (e.g., <356, 499>) are PLACEHOLDERS to show the JSON shape and y-x ordering. Different episodes have different object layouts. Re-ground every coordinate from the actual current image. never copied from the example."
| 지표 | 3279 baseline | 3317 anti-parrot | Δ |
|---|---|---|---|
| SR | 3/50 = 6.0 % | 3/50 = 6.0 % | 0 pp |
| Errors | 0 | 0 | — |
| Parrot-dominant eps (>50% parrot calls) | 29/50 = 58 % | 13/50 = 26 % | -32 pp |
| Per-call parrot rate | ~50 % | 29.7 % | -20 pp |
| Successful eps | 1, 40, 41 | 4, 25, 40 | 다른 ep set |
| Wall-clock | 1 h 5 min | 1 h 18 min | +13 min |
gemini-2.5-pro 로 BinFill 50 ep 재시도 (scripts/run_api_mem_one_episode.sh
BinFill gemini-2.5-pro). pro 도 default 가 thinking ON 이라 truncation 우려가
돌아올 수 있음 — extra_body 다시 추가하지 말고 일단 max_tokens=2048 로 올려서
thinking + visible 모두 수용. parse_fail rate 보고 재조정.runs/api_mem_smoke/BinFill_3279_archive/ (수동 archive 권장)runs/api_mem_smoke/BinFill/(다음 run 시작 전에 위 두 디렉토리를 보존해 두면 ablation 비교 데이터 재사용 가능)
직전 baseline 6 % + anti-parrot 6 % 에서 grounding 정확도가 bottleneck 으로 결론. 두 fix 동시 시도:
- A. gemini-2.5-pro (vision ↑, 비용 ↑): tag=pro, save_dir=runs/api_mem_smoke/BinFill_pro/
- B. flash-lite + 256→512px LANCZOS upscale (model 그대로, image ↑): tag=upscale512, save_dir=runs/api_mem_smoke/BinFill_upscale512/
examples/robomme/subgoal_prediction/api_mem/api.py: image_upscale: int param + _make_image_part(img, upscale_to) LANCZOS resize.examples/robomme/eval.py: api_mem_image_upscale: int = 0 flag.examples/robomme/subgoal_predictor.py: pass through.scripts/run_api_mem_one_episode.sh: 4 args = <task> [model] [tag] [upscale]. save_dir 에 _<tag> suffix.max_tokens 1024 → 2048 (pro 의 thinking 흡수 용도).| 지표 | pro 5/50 ep |
|---|---|
| done | 5/50 (preempt 후 sbmr 5 exhausted) |
| SR | 0/5 = 0 % |
| Errors | 0 |
| Parse fails | 0/115 calls |
| Parrot rate | 0.9 % (vs flash-lite anti-parrot 30 %) |
| Latency | 9 ~ 11 s/call |
pro 는 anti-parrot 거의 완벽 (vs flash-lite 의 30 %). subgoal coords 가 episode 마다 모두 다른 fresh 값. mt 도 정상 진화 (I have picked up the red cube.). 다만 5 ep 표본 너무 작아 SR 판단 보류 — 4314 (sbmr 10 으로 재제출) 결과 대기.
| 지표 | upscale512 1/50 ep |
|---|---|
| done | 1/50 (model 가 망가져서 restart 10번 모두 fail → MAX_CLIENT_RESTARTS exit) |
| SR | 0/1 |
| Errors | 1 |
| Parse fails | 40/240 calls (17 %) |
| Failure mode | model 이 "I have picked 0 cubes." 무한 반복 (=degenerative loop) |
중요 발견 — 256→512 LANCZOS upscale 은 flash-lite 를 망가뜨림. 모델이 같은 문장을 max_tokens 까지 반복하는 degenerative loop 에 빠짐. 가설: - LANCZOS resampling 이 256×256 native 이미지를 512×512 로 키우면서 texture 정보 없는 평탄한 픽셀 영역이 늘어남 → image token 의 정보 밀도 감소 → 모델이 텍스트 출력으로 "더 많이 생각하려 시도" → repetitive loop. - 또는 LetSur gateway 가 더 큰 base64 payload 에 다른 처리 적용. 이 variant 폐기, 다른 image preprocessing (e.g. center-crop instead of upscale, or send native 256 with explicit zoom-in coordinates description) 가 필요.
| Run | Model | parrot rate | parse fail | SR | 비교 baseline |
|---|---|---|---|---|---|
| MemER LoRA | Qwen3-VL-4B + LoRA (in-domain) | n/a | n/a | 64.0 % (32/50) | (외부 baseline) |
| 3279 baseline | flash-lite | ~50 % | 0 % | 6.0 % (3/50) | -58 pp |
| 3317 anti-parrot | flash-lite + prompt fix | 30 % | 0 % | 6.0 % (3/50) | -58 pp |
| 4283 upscale512 | flash-lite + 512 LANCZOS | n/a | 17 % (loop) | 0/1 (broken) | n/a |
| 4314 pro | gemini-2.5-pro | 3.0 % | 0.8 % | 4.0 % (2/50) | -60 pp |
Pro 의 latency 11 s/call (vs flash-lite 2-3 s) — 5x 비싸고 5x 느린데 SR 더 낮음.
LoRA success eps (32 개): 0,1,3,4,5,6,9,12,16,17,20,21,22,23,24,25,27,28,29,31,32,34,36,37,40,41,42,43,44,45,...
| Variant | success eps | LoRA 도 success? |
|---|---|---|
| flash-lite baseline | 1, 40, 41 | 모두 ✅ (LoRA-easy 만 품) |
| flash-lite anti-parrot | 4, 25, 40 | 모두 ✅ (LoRA-easy 만 품) |
| pro | 16, 18 | 16=✅, 18=❌ (pro 가 LoRA 가 못 푼 ep 1개 품) |
→ ApiMem 의 success 는 거의 모두 LoRA 도 푸는 쉬운 ep 의 부분집합. ApiMem 만의 강점 ep 는 50 ep 중 1 개 (ep18).
pro ep0 (LoRA success, pro fail) 의 trajectory:
call 0: pick first red cube at <453, 248> mt=''
call 1: put it into the bin at <665, 525> mt='I have picked up the red cube.'
[episode ends as fail with only 2 calls]
pro ep18 (pro success, LoRA fail) 의 trajectory:
15 calls, 11 distinct subgoals, mt 정상 진화 (picked one → put one → picked two →
put two → press button → task complete → reset → 재시도)
→ pro 의 mt + grounding 자체는 정확히 작동 (ep18 처럼 풀 수 있음). 그러나 ep0 같이 sim 의 success-predicate / fail-detector 가 매우 일찍 firing 해서 pi0.5 가 충분한 시도 기회를 못 갖고 종료. 같은 정보를 받았을 때 LoRA 는 따라가는데 pi0.5+API 조합은 못 따라감 → wording / coord normalization / timing 중 어딘가의 학습 분포 mismatch.
BinFill 은 LoRA 가 64 % 까지 풀 정도로 학습 분포에 잘 맞는 task — ApiMem 의 zero-shot strength (큰 모델 reasoning) 이 의미 있게 발휘될 영역이 아니다. 격차 60 pp 는 prompt tweak 으로 못 메움.
대신 LoRA 도 약한 task 로 옮겨서 ApiMem 이 의미 있는 영역 찾는다. 가설: - PatternLock (LoRA 16 %): 긴 sequence + memory-of-positions-pressed → mt 의 자연어 카운팅이 도움 가능. ApiMem ≥ 30 % 면 의미 있음. - ButtonUnmaskSwap (LoRA 18 %): swap 시점 추적 → mt rule 2 (변경 없으면 unchanged) + grounding flexibility 가 도움 가능. - InsertPeg / StopCube (LoRA 4 %): precision-critical, ApiMem 이 도움 줄 가능성 낮음.
가장 cheap 한 alignment test: API 가 emit 한 subgoal 을 oracle ground-truth 좌표로 overwrite 한 후 pi0.5 에 넣음. 만약 그래도 SR 낮으면 wording mismatch, 높으면 coord 정확도 문제로 분리 가능. 단 oracle 좌표 추출 자체가 일이라 우선순위 낮음.
사용자 피드백으로 4 runs 모두 ApiMem path 에 coord scaling 단계 누락 확인.
| Path | emit scale | 256-space rescale? |
|---|---|---|
MemER LoRA (api_memer.py:_parse_subgoal_for_vla) |
1000-norm | ✅ <x*256/1000, y*256/1000> |
Gemini (gemini/api.py:normalize_point_in_response) |
1000-norm | ✅ _normalize_point |
ApiMem (api_mem/api.py:get_subgoal) |
1000-norm | ❌ no rescale (raw 1000 returned) |
→ pi0.5 가 받는 좌표가:
- LoRA: pick up at <91, 127> (256-space, valid pixel)
- ApiMem 직전까지: pick up at <356, 499> (1000-space — 256x256 image 에서는
out-of-range 4 배 어긋남)
위 4 runs 의 SR 6 %, 4 %, 0 % 는 LoRA-vs-ApiMem 격차가 아니라 ApiMem path 의 broken-pipe 결과. grounding/parroting 가설이 SR 변동 없었던 이유도 설명됨 — 어떤 좌표를 emit 하든 어차피 4x scale 어긋나서 모두 fail.
examples/robomme/subgoal_prediction/api_mem/api.py 에 _rescale_coords 메소드
추가, get_subgoal 의 return 직전에 적용:
def _rescale_coords(self, subgoal: str) -> str:
h, w = self.image_size # (256, 256)
def repl(m):
y = int(int(m.group(1)) * h / 1000)
x = int(int(m.group(2)) * w / 1000)
return f"<{y}, {x}>"
return re.sub(r"<\s*(\d+)\s*,\s*(\d+)\s*>", repl, subgoal)
mt, last_subgoal 은 1000-scale 그대로 보존 (다음 호출 prompt 에 들어가는 값).Unit test 4/4 OK:
'pick up the first red cube at <356, 499>' -> '<91, 127>'
'put it into the bin at <500, 500>' -> '<128, 128>'
'press the button at <0, 1000>' -> '<0, 256>'
'two coords <100,200> and <800, 600>' -> '<25, 51>' '<204, 153>'
runs/api_mem_smoke/BinFill_rescaled/| 지표 | 4429 rescaled |
|---|---|
| done | 50/50 |
| SR | 8.0 % (4/50) |
| success eps | 12, 25, 28, 42 (모두 LoRA-success ep) |
| Errors | 0 |
| Parse fails | 0/997 |
| Parrot rate | 41.3 % |
| Run | Model | Coord | Anti-parrot prompt | SR | success eps | parrot rate |
|---|---|---|---|---|---|---|
| 3279 baseline | flash-lite | broken pipe | ❌ | 6.0 % | 1, 40, 41 | 50 % |
| 3317 anti-parrot | flash-lite | broken pipe | ✅ | 6.0 % | 4, 25, 40 | 30 % |
| 4283 upscale512 | flash-lite + 512px | broken pipe | ✅ | 0/1 (loop fail) | — | n/a |
| 4314 pro | gemini-2.5-pro | broken pipe | ✅ | 4.0 % | 16, 18 | 3 % |
| 4429 rescaled | flash-lite | fixed (1000→256) | ✅ | 8.0 % | 12, 25, 28, 42 | 41 % |
| MemER LoRA | Qwen3-VL+LoRA (in-domain) | n/a | n/a | 64.0 % | 32/50 | n/a |
BinFill 떠나서 LoRA 가 약한 task 에서 ApiMem 이 의미 있는 영역 찾기. 우선순위: - PatternLock (LoRA 16 %): 긴 sequence + memory-of-positions-pressed → mt 카운팅이 도움 가능 - ButtonUnmaskSwap (LoRA 18 %): swap 시점 추적 → mt rule 2 유리 - InsertPeg (LoRA 4 %): precision-critical, ApiMem 도 어려울 가능성 높음
각 task × pro 50 ep × sub partition. ≥ 30 % 나오면 Phase 3 ablation tier (mem / no-mem / LoRA) 비교 진행.
prompts/base.py 에 bbox-first 3-step procedure 강제:
{
"scratchpad": "...",
"detected_objects": [
{"label": "first red cube", "box_2d": [y1, x1, y2, x2]},
{"label": "bin", "box_2d": [...]},
{"label": "button", "box_2d": [...]}
],
"memory_summary": "...",
"subgoal": "pick up the first red cube at <y_center, x_center>"
}
detected_objects 에 모든 target 의 1000-norm bbox emit.((y1+y2)//2, (x1+x2)//2) 계산.OUTPUT_FORMAT_WITH_MEMORY 도 새 필드 포함하게 수정.| 지표 | 5092 bbox | 4429 rescaled (직전 baseline) |
|---|---|---|
| done | 18/50 (script abort) | 50/50 |
| SR | 16.7 % (3/18) | 8.0 % (4/50) |
| success eps | 1, 12, 25 (모두 LoRA-success) | 12, 25, 28, 42 |
| Errors | 1 | 0 |
| Parse fails | 7.8 % (38/485 calls) | 0 % (0/997) |
| bbox emit rate | 99.8 % | n/a |
Success eps 의 bbox quality 깨끗: 3-4 객체, center 계산 정확
- ep1 call 0: first green cube bbox [356, 100, 420, 156] → center <388, 128>
→ subgoal 'pick up the first green cube at <388, 128>' ✅
- ep12 call 0: first blue cube bbox [408, 475, 508, 549] → center <458, 512> ✅
그러나 fail 시 catastrophic loop: model 이 100+ 가상 객체를 enumerate
"...The ninety-eighth blue cube is at <372, 7140>.
The ninety-ninth blue cube is at <372, 7212>.
The one hundred and sixth red cube is at <472, 700>..."
- bbox + reasoning + memory_summary 동시 생성이 flash-lite 의 cognitive
capacity 초과. max_tokens 2048 까지 허위 객체 nested loop 으로 채워버림.
- 38 parse_fails 중 대부분이 이 패턴.
- 18 ep 만에 client process 가 10 restarts 다 소모하고 script 가 정상 종료.
그래도 SR 8% → 16.7% (제한된 18 ep sample, +8.7 pp). 통계적 유의성은 작은 sample 이라 약하지만 명확한 signal. bbox 가 grounding 정확도 올린 게 맞음.
prompts/base.py 에 DETECTION_SYSTEM_PROMPT + DETECTION_USER_PROMPT 추가.
GROUNDED_COORD_INFO 도 user-message 로 들어오는 detected_objects 를 참조하게
변경.prompts/__init__.py 에 per-task DETECT_HINTS dict + build_detection_system_prompt.api.py 에 _call_detection(image) 메소드 + get_subgoal refactor — 매 tick
마다 detection call → reasoning call (with detected_objects in user msg).| 지표 | 5652 2-step | 5092 1-call bbox | 4429 rescaled |
|---|---|---|---|
| done | 47/50 (RUNNING) | 27/50 (loop abort) | 50/50 |
| SR | 6.4 % (3/47) | 16.7 % (3/18) | 8.0 % (4/50) |
| success eps | 1, 25, 44 (모두 LoRA-success) | 1, 12, 25 | 12, 25, 28, 42 |
| degenerative loops | 0 ✅ | 8/27 ❌ | 0 |
| parse_fail rate | 0.4 % ✅ | 7.8 % ❌ | 0 % |
| det_empty rate | 4.1 % | n/a | n/a |