Index
2026-08-06 — Experiment

M4 — 학습된 per-view camera token으로 PRoPE 대체

MetaView → WAM Backbone | stage 2 검증 + 제출 (job 72911)

TL;DR

137
tests passed
0.000e+00
check_noop
9.908e-02
S4.5 격리 검증
2
게이트 결함
8000
steps (진행 중)

1 배경 / 목적

Phase 1 세 arm이 완주하면서 구조적인 결론이 나왔다. 정합 112개 검증 지점(step ≥ 1000) 평균:

arm메커니즘wrist LPIPS후반 (step ≥ 6000)wrist PSNR배포
M1없음0.25790.256818.52
M2PRoPE key만0.26220.254618.45
M3PRoPE query+key, GT wrist pose0.14380.115021.74✗ oracle
M2는 메커니즘의 절반만 돌고 있었다. PRoPE는 relative encoding이라 q가 P_qᵀ를, k가 P_k⁻¹를 실어야 qᵀk에 상대 변환이 남는다. 한쪽에만 걸면 상대 항이 아예 없다. 그리고 이 메커니즘의 절반은 값어치가 0이었다.

문제는 M3가 배포 불가능하다는 것이다. inference 시점에 wrist 카메라의 미래 pose는 모델이 예측해야 할 대상 그 자체 — 즉 action이다. 그래서 M4는 어떤 카메라의 pose도 입력으로 받지 않으면서 카메라 정보를 retrieval에 넣어야 한다. M1→M3 격차(0.2579 → 0.1438)가 회수 목표이고, M4의 판정 기준은 M3가 아니라 M1/M2다.

2 작업 내용 — 실체는 세 군데

1. src/wam/camera_attention.py (신규) — frame-wise attention

X-WAM은 세 view를 한 시퀀스로 합쳐 full self-attention을 돌리므로 AA의 global 절반은 이미 공짜로 있다. 없는 건 frame-wise 쪽 — cam[v,t]가 자기 view/frame만 보게 강제하는 것.

grid = rearrange(video, "b (t v hw) d -> b t v hw d", t=num_frames, v=num_views, hw=hw) kv = grid[:, self.frame_ids, self.view_ids] # (b, n_cam, hw, d) q = rearrange(q, "b n (h e) -> (b n) h 1 e", h=num_heads) # token 축을 batch로 접음
frame_ids/view_ids 두 인덱스 텐서의 짝이 이 모듈 전체다. 짝이 틀리면 모든 camera token이 다른 view 내용을 학습하는데 shape·dtype·loss 어디에도 증상이 없다.

2. src/wam/geometry_branch.py:244-258 — q/k projection에 camera 주입

q = q + self.to_cam_q(wrist_camera_broadcast(cam_in, num_frames, hw)) k_3d_flat = k_3d_flat + self.to_cam_k(exo_camera_broadcast(cam_in, num_frames, n3d // num_exo)) k_ref_flat= k_ref_flat+ self.to_cam_k(exo_camera_broadcast(cam_in, num_frames, hw))

key로 덧붙이지 않고 projection에 더한다. 덧붙이면 query가 카메라를 내용으로 읽을 뿐, 3D token을 어떻게 고를지는 안 바뀐다. v에는 일부러 안 넣었다 — 카메라는 retrieval을 조건화하지 content를 바꾸지 않아야 하고, v에 넣으면 attention이 뭘 골랐든 카메라 벡터가 residual stream에 그대로 주입된다.

broadcast가 셋인 이유: query는 wrist slice라 frame-major(repeat_interleave), key 두 그룹은 exo t=0 내용이라 view-major이고 3D grid(encoder)와 latent patch grid가 서로 다르다. 셋 중 뭘 바꿔 끼워도 shape이 맞아 에러가 안 난다 → 테스트가 (t v hw) 레이아웃에 직접 assert.

3. src/wam/model.py:192-209_GeometryHook.apply, write-back

cam = input_seq[:, n_video + Ta:] cam = cam + self.camera_attentions[block_index](cam, video, num_frames=num_frames) residual = self.branches[block_index](video, self.tokens_3d, ..., cam=cam) return torch.cat([video + residual, input_seq[:, n_video:n_video + Ta], cam], dim=1)

refine한 camera token을 input_seq다시 쓴다. 로컬 변수로 버리면 다음 block의 global attention이 stale token을 보고, alternation이 아니라 독립 pass 30번이 된다. hook 안에서 frame-wise가 branch보다 먼저 오는 것도 의도 — 같은 block의 추정치로 conditioning된다.

camera token은 6차원이 아니다

proprio_dim: 6은 양 끝단뿐이다. X-WAM의 기존 proprio_encoder (Linear(6,3072)+GELU+Linear(3072,3072))/proprio_decoder가 그대로 쓰이고, 시퀀스 안에서는 (B, 9, 3072)다. view embedding은 encoder 통과 dim 공간에서 더한다 — 6D 안에는 신원 표식 자리가 없고, 넣으면 supervise할 양 자체가 오염된다.

그리고 이 slot은 입력이 아니라 flow-matching 타깃이다. pose가 노이즈된 상태로 들어가고 velocity를 예측한다. 그래서 oracle이 아니다.

3 결과

검증

항목
테스트137 passed (75s)
check_noop zero-init0.000e+00 (video + proprio)
positive control1.448e-02 — 설계문서 기대값과 일치
M4 no-op (같은 9-token slot, hook on vs bypass)0.000e+00 양쪽
hook 실행30 block 전부, 0..29 순서대로
camera token drift block 0↔296.838e+01 (write-back 작동)
branch conditioning 격리 (S4.5)9.908e-02
M2↔M4 config diff정확히 3필드
패치 파일 ↔ X-WAM-metaview clone65줄 완전 일치

검증 중 발견한 결함 — 둘 다 코드가 아니라 게이트 쪽

1. check_noop이 증명 못 하는 걸 단언하고 있었다.
frame-wise attention만 살렸을 때 video 출력이 2.051e-02 변하는 걸 두고 "branch가 camera token으로 conditioning된다"고 assert했다. 그 시점에 branch의 to_out은 여전히 zero-init이라 branch 기여분이 정확히 0이다. 그 변화는 전부 다른 경로 — camera token이 proprio slot에 있고 X-WAM 자체 global attention이 그걸 읽는다. M2가 "뭔가 움직였으니 작동한다"고 믿었던 것과 같은 함정.
수정: to_cam_q/to_cam_k만 다르고 to_out은 양쪽 다 살린 두 run 비교 → 9.908e-02.
2. scripts/run_wam_m4.sh가 아예 없었다.
테스트 137개 통과 + check_noop 통과인데 arm을 클러스터에 올릴 방법이 없었다. 스크립트를 만들고, arm마다 run 스크립트 존재 + 자기 config를 가리키는지 검사하는 테스트를 추가했다. 이 스크립트들은 이전 arm 걸 복사해 만들어서, 이름은 m4인데 마지막 줄은 m3 config를 돌리는 게 실제로 일어나는 실패다(그러면 두 arm이 한 output_dir을 두고 싸운다). 두 방향 mutation test로 실패 확인.
3. 설계문서 §6의 step-0 동일성 게이트가 M4에는 성립 불가.
M1/M2/M3는 셋 다 lpips=0.692614로 같지만, M4는 proprio slot이 3→9로 넓어져 모든 block이 attend하는 시퀀스가 바뀐다. 그대로 적용하면 오탐이다.

제출 및 초기 상태

job 72911 · sbmr 30 "bash scripts/run_wam_m4.sh" --qos=extra --gres=gpu:4 -c 32 --mem 800GB --time=36:00:00 [distributed] world_size=4 devices=4 effective_batch=32 [metrics step=0] wrist: psnr=14.67 ssim=0.4533 lpips=0.6907 pose: rot_deg_wrist=72.78 rot_deg_exo1=125.38 rot_deg_exo2=109.13 peak_mem=39.8GB
step-0 wrist LPIPS 0.6907 vs M1/M2/M3의 0.692614 — 같은 4 GPU, 같은 설정인데 다르다. 위 3번 예측의 실측 확인이고, 차이 0.0019는 "549 토큰 시퀀스에 6개 추가"와 부합하는 크기다. hook 자체가 무해하다는 건 check_noop이 같은 9-토큰 slot으로 이미 0.000e+00을 보여줬다.

4 Takeaway

지각(perception)과 조건화(conditioning)는 직교하는 두 결정이고, M4는 지각 쪽만 원설계를 따랐다. camera token + AA는 "GT 없이 카메라를 어떻게 얻나"의 답이고, additive는 "얻은 카메라를 어떻게 넣나"에 대한 별개 선택이다. 후자가 약한 고리다(§5).

Phase 1의 진짜 교훈은 리뷰가 결함을 못 잡았다는 것

테스트 65개, 비트 정확한 check_noop, positive control까지 전부 초록이었는데 M2는 메커니즘의 절반만 돌고 있었다. 드러난 건 사용자가 "카메라 파라미터가 어떻게 들어가고 있냐"고 물었을 때였다.

이번에도 같은 계열의 두 결함이 "전부 통과" 상태에서 살아남았다 — 증명 못 하는 걸 단언하는 assert, 그리고 제출 경로가 아예 없는데 아무도 안 보는 것. 통과하는 게이트는 그 게이트가 무엇을 배제하는지 확인하기 전까지 증거가 아니다.

5 보완점 / 다음

additive 조건화의 표현력 한계 (미해결, 실측 전)

q_c는 frame당 한 벡터를 60개 patch가, k_c는 view당 한 벡터를 모든 3D token이 공유한다. logit을 펼치면:

L[i,j] = q_v[i]·k_v[j] ← i,j 모두 fine-grained. 근데 카메라가 없음 + q_v[i]·k_c[v] ← i는 fine, j는 view 단위 상수 + q_c[t]·k_v[j] ← i는 frame 단위 상수, j는 fine + q_c[t]·k_c[v] ← (frame,view)당 스칼라 하나

PRoPE는 q_v[i]ᵀ P_t P_v⁻¹ k_v[j] — 카메라가 bilinear form 자체를 바꿔 모든 (i,j) 쌍이 joint하게 변형된다. Additive는 카메라가 rank-1 bias로만 들어간다: i에 fine-grained하거나 j에 fine-grained하거나 둘 중 하나지, 동시에는 안 된다.

이게 하필 이 branch에서 치명적인 이유: geometry branch가 푸는 건 "pixel (u,v)의 wrist patch가, 카메라 pose가 P일 때, 어느 3D token을 읽어야 하나" — query 위치 × key 위치 × relative camera의 3자 결합, 즉 ray correspondence다. Additive가 쓸 수 있는 건 q_c[t]·k_v[j]("내 카메라가 여기 있을 때 이 3D token들이 대체로 중요")까지고, "내가 그 중 이 픽셀일 때 저 3D token"은 못 쓴다.

설계문서에 q_camᵀk_cam을 "PRoPE의 학습판"이라 쓴 것은 과했다 — 그 항은 (frame, view)당 스칼라 하나다. 남은 경로는 간접적이다: camera token이 input_seq에 있으니 global attention이 30 block에 걸쳐 q_v 자체에 카메라를 섞어 넣으면 항 1이 full-rank로 카메라 의존적이 된다. 다만 이건 DiT가 그 섞기를 스스로 학습해야 성립한다.

결판 실험(제안, 미실행): M3-additive. M3의 GT wrist pose를 그대로 쓰되 PRoPE 대신 additive로 넣는다. 양쪽 다 완벽한 pose를 받으므로 지각 품질이 변수에서 빠지고 조건화 형태만 남는다. M1↔M3 격차는 step 1000쯤부터 112개 중 111개에서 보였으므로 2000 step(~5h)이면 갈린다. 사용자 판단으로 M4를 먼저 그대로 돌리기로 함.

결과 판독 기준

M4가 0.25 근처면 M2와 같은 자리 = 조건화 미작동. 그때 진단 순서:

  1. camera token linear probe — exo2와 wrist만. exo1은 제외: relativize가 index 0에 np.eye(4)를 쌓아 world frame = exo1이므로 타깃이 정확히 [0 0 0 0 0 0](5 clip 전부 max|V[0]−I| = 0). 9개 토큰 중 3개는 0만 뱉으면 loss가 0이라 대조군이 될 수 없다. probe가 정확한데 LPIPS가 안 움직이면 → 지각은 됐고 branch가 못 쓴 것.
  2. static exo drift — exo 카메라 3개 토큰이 프레임 간 일치하는지. 이건 exo1에도 유효하다(타깃이 뭐든 정적 카메라의 세 토큰은 같아야 한다).
  3. branch/ratio_max, attn_max — M2가 step 2030에서 붕괴한 구간 (wrist 0.30→0.61, exo는 멀쩡, 600 step 미회복, 재현 안 됨)을 M4도 지나간다. M2 최종값 ratio_max 0.407 / attn_max_max 13.9.