TL;DR
- 실패 원인 확인: apimem-tools-v1 LLM agent SR=0 on BUS — tool hallucination + coord instability + subgoal sequence 추적 불가. 6 fix patch 후에도 SR=0 → branch hard-reset.
- Pivot 결정: LLM을 discrete action selector로 축소, code가 state machine 구동. SAM2로 container tracking, GroundingDINO로 anchor detection. LLM은 타깃 색상 선택 2회만.
- 구현 완료: Stages 1-8 (12 commits) + 4 bug fix commit.
BUSPipelinePredictor 전체 파이프라인 E2E 연결 완료.
1 배경 / 목적
apimem-tools-v1 (P2-P4.1) — LLM이 annotate_objects / track_objects / save_keyframe tool을 직접 호출하는 multi-turn agent — 이 VideoUnmaskSwap(VUS) 50ep sweep을 위해 구현·제출됐다. 동시에 ButtonUnmaskSwap(BUS)에도 적용해봤으나 결과는 SR=0.
실패 진단 (6 fix patch 후에도 SR=0):
- (a) Tool hallucination: LLM이 존재하지 않는 tool을 호출하거나 잘못된 인자 형식 사용
- (b) Coord instability: LLM이 매 tick마다 다른 좌표를 emit → VLA가 일관된 target 못 받음
- (c) Subgoal sequence 추적 불가: vision만으로 "버튼 1 눌렀는가 → 2 눌렀는가 → swap 완료됐는가"를 신뢰성 있게 판단 못 함
BUS의 핵심 insight: 버튼 누르기는 위상 전환을 gate하지 않는다. 컨테이너 drop + swap은 시뮬레이터가 agent와 병렬로 자동 실행한다. 즉 6-step 시퀀스는 결정론적이고, 유일하게 LLM이 필요한 시점은 task_goal text에서 타깃 색상을 추출하는 steps 3/5뿐이다. LLM-driven agent 구조는 과설계였다.
2 작업 내용
Pivot 결정: Code-Orchestrated, Task-Specific Pipeline
Branch를 6c93bcb (P0/P1 SAM2+GroundingDINO 안정 상태)로 hard-reset. 새 접근의 핵심 3가지:
핵심 설계 결정
────────────────────────────────────────────────────────
1. Code drives state machine / LLM picks ONE discrete action
→ LLM은 좌표 emit 없음. code가 anchor lookup → subgoal text 생성
→ tool hallucination / coord instability failure class 제거
2. Memory = single post-swap keyframe (code가 SAM2로 생성)
→ SAM2 obj_id로 swap 전 container 위치 ↔ cube 색상 mapping 보존
→ LLM은 keyframe 보고 색상만 pick (단순 VQA)
3. Action menu = 6-step fixed sequence
Steps 1-2: press_button_1/2 (고정, LLM 호출 없음)
Steps 3/5: pick_container_ (regex parse task_goal, LLM 필요 시만)
Steps 4/6: put_down (고정)
────────────────────────────────────────────────────────
모듈 구조 (신규 파일, bus_pipeline/)
| Stage | 파일 | 역할 | Commit |
| Stage 1 | anchor_registry.py | AnchorRegistry 데이터 클래스, coord 저장/조회 | 420ccab |
| Stage 2 | phase_tracker.py | 6-step 시퀀스 state machine + PhaseTracker | d1cc710 |
| Stage 3 | phase_tracker.py | Phase A detection — GroundingDINO로 button + cube anchor 추출 | a643516 |
| Stage 4 | swap_tracker.py | SwapTracker — SAM2로 swap window ~96 frames container 추적 | b1429ae |
| Stage 5 | action_selector.py | ActionSelector — task_goal regex parse, fallback LLM call | d92ecbe |
| Stage 6 | orchestrator.py | BUSPipeline 메인 클래스 — phase 전환 + subgoal text 생성 | 58e564b |
| Stage 7 | subgoal_predictor.py | BUSPipelinePredictor + eval.py Args 연결 (use_bus_pipeline flag) | 7fe4fa1 |
| Stage 8 | - | Step gating + seed validation + per-tick viz dump | 5f39884 |
Bug Fix Commits (Stage 8 이후)
502dfd5 BUS fix: rescale 1000-scale subgoal → image-pixel for VLA
(GroundingDINO 출력이 1000-scale, pi0.5는 256px space → rescale 누락)
077bbe9 BUS fix: relax seed score threshold 0.35 → 0.20 + log raw detections
(Phase A에서 GroundingDINO가 container 못 잡는 경우 → threshold 낮춤)
101354e BUS rearchitect: SAM2 tracks CUBES from t=0, not containers post-drop
(컨테이너가 cube 위에 떨어진 후 추적하면 occlusion 때문에 실패
→ t=0부터 cube를 추적해서 drop 후에도 container 위치 파악)
10d85af BUS fix: dense per-env-step frame buffer for SAM2
(SAM2에 넘기는 frame이 sparse하면 swap motion 중 tracking drift
→ env step마다 frame append, dense buffer 유지)
SAM2 Tracking 전략 변경 (rearchitect)
당초 설계는 "컨테이너가 cube 위에 drop된 후 3개 컨테이너를 SAM2로 추적"이었다. 문제는 컨테이너가 처음 등장할 때 occlusion이 심해서 SAM2 seed bbox가 불안정하다는 것. 해결책: t=0부터 cube 자체를 추적한다. Cube는 처음부터 fully visible, GroundingDINO가 정확히 잡는다. Drop 후 cube 위치 = 컨테이너 위치 → cube obj_id를 컨테이너 identity로 그대로 이어받는다.
3 결과
구현 완료 현황
| 항목 | 상태 | 비고 |
| Stages 1-8 구현 | 완료 | 12 commits, bus_pipeline/ 6 파일 |
| rescale bug fix | 완료 | 1000→256px, pi0.5 coord 호환 |
| SAM2 rearchitect (cube t=0) | 완료 | occlusion 문제 구조적 해결 |
| dense frame buffer | 완료 | swap motion tracking drift 방지 |
| 10-ep BUS sweep | 대기 | Stage 9: sbm 제출 필요, 목표 SR ≥ 30% |
| apimem-tools-v1 VUS sweep | 진행 중 | VideoUnmaskSwap 50ep, 별도 트랙 |
구조적 장점: 코드가 state를 완전히 제어하므로 LLM cost ≈ 0 (regex parse 성공 시 LLM 호출 없음). GroundingDINO + SAM2는 로컬 GPU에서 무료. 50ep 예상 비용 < $0.10.
미검증: 실제 BUS episode에서 GroundingDINO seed score와 SAM2 tracking이 얼마나 안정적인지는 smoke run (Stage 8)에서만 확인. 10-ep sweep 결과가 없는 상태.
Action Menu → VLA Subgoal Text 매핑
press_button_1 → "press the first button at <y, x>"
press_button_2 → "press the second button at <y, x>"
pick_container_red → "pick up the container at <y, x> that hides the red cube"
pick_container_blue → "pick up the container at <y, x> that hides the blue cube"
pick_container_green → "pick up the container at <y, x> that hides the green cube"
put_down → "put down the container"
※ subgoals_grounded in prompts/ButtonUnmaskSwap.py — 학습 분포 exact match
4 Takeaway
LLM agent의 한계와 task-specific 코드 오케스트레이션의 가치
BUS는 사실 LLM이 필요 없는 task다. 6-step 시퀀스는 결정론적, 좌표는 detection에서 바로 나오고, 타깃 색상은 task_goal text의 regex 파싱으로 충분하다. LLM-driven tool-calling agent는 이 단순한 문제를 "open-ended agentic loop"로 복잡하게 만들어 hallucination / instability가 발생했다.
이 pivot의 진짜 의미는: tool-calling agent가 적합한 task와 그렇지 않은 task를 구분해야 한다는 것. BUS는 코드 오케스트레이션, VideoUnmaskSwap은 다단계 VQA가 필요한 task로, 각자 다른 접근이 맞다. 두 track을 병행 운영하는 현재 구조가 올바른 방향.
SAM2 rearchitect (cube t=0 추적) insight: container가 아닌 cube를 처음부터 추적하는 것은 occlusion에 강건한 identity preservation 방법. swap 후에도 cube obj_id가 곧 "어느 컨테이너 아래에 어떤 색상의 cube가 있는가"를 직접 인코딩한다. 이 패턴은 다른 shell-game task (VideoUnmaskSwap 등)에도 일반화 가능.
5 Next Steps
Stage 9: BUS 10-ep Sweep 제출
sbmr 5 "bash scripts/run_bus_pipeline.sh ButtonUnmaskSwap 0 10" --gres=gpu:2 -c 16 --mem=400GB --qos=extra
성공 기준: SR ≥ 30% (MemER LoRA 18% 대비 개선). per-phase pass rate 로그 분석으로 실패 지점 파악.
미해결 리스크: Phase A detection 신뢰성
GroundingDINO seed score threshold 0.20으로 낮췄지만 여전히 "버튼 2개 + cube 3개"를 ep마다 안정적으로 잡는지 불명. 10-ep sweep 로그의 event=phase_transition과 raw_detections로 확인. 만약 phase A에서 stick이 많으면 Gemini-backed detection을 fallback으로 추가.
VideoUnmaskSwap tool-calling sweep 병행
apimem-tools-v1 VUS 50ep sweep은 별도 트랙. SR 결과에 따라 VUS에서도 code-orchestrated pivot이 필요한지 판단.