Index
2026-08-07 — Analysis

pointmap 인코딩 재설계 — 하드 박스 vs monotone squash

X-WAM | DROID world-frame pointmap A/B · 260805 확정 설계의 재검증

TL;DR

47.2%
box sentinel
8.5%
linlog sentinel
+0.4mm
workspace 손실 (+VAE)
3.1M
측정 픽셀

1 배경 / 목적

260805에 확정한 pointmap 인코딩은 box B 하드 클립이다. 박스 밖 픽셀은 sentinel(0)이 되고, 미니셋 실측 pixel-invalid가 56.8%였다. 스모크 리포트에 "최대 리스크"로 남겨둔 항목이 정확히 이것이다.

기존 한계(260805 시점의 서술): X-WAM의 depth loss에는 mask가 없다 — depth_loss = (pred-gt).pow(2).mean(). 즉 depth 브랜치 1.64B 파라미터 용량의 절반 이상이 상수 −1을 출력하는 법을 배우는 데 쓰인다. §5에서 정정됨 — 상수는 배우기 쉬워서 loss 값은 먹어도 용량은 거의 안 먹는다.

계기는 사용자가 제시한 VLA pointmap 논문 See like a Robot: Robot-Centric Pointmaps for VLA(anonymous, π0.5/SmolVLA + RoboCasa)였다. "이 논문은 pointmap을 48%씩 잘라내지 않지 않냐"는 지적 — 맞고, 이유가 결정적이다.

논문이 자르지 않는 이유 = 입력 vs 출력. 논문은 pointmap을 인코더 입력으로만 쓴다(z_c = fθ(I_c) + gφ(PEE_c), SigLIP 복사본이 토큰화해 RGB 토큰에 element-wise add). 예측하지 않으므로 복원 요구가 없고 → 8-bit mp4가 필요 없고 → 박스도 sentinel도 필요 없다. 실제로 본문에 정규화·클리핑 언급이 한 줄도 없다.

우리는 pointmap이 생성 타깃이라 사정이 다르다. 그러나 "박스"가 정말 필수인지는 검증한 적이 없었다. 박스가 강제하는 것은 '균일 양자화'이지 '되돌릴 수 있음'이 아니다 — 단조 함수면 무엇이든 정확히 역산된다.

2 작업 내용

인코딩 스킴 3계열

모두 px = 1 + round(n·254), 0은 invalid sentinel, 복원은 n = (px−1)/254. 차이는 n을 공간에 어떻게 배분하느냐뿐이다.

스킴정의버려지는 픽셀
box (현행)축별 선형, box B 밖 → sentinel박스 밖 전부
linlog f코드 공간의 f를 박스 안에 선형, 나머지 1−f를 ±8m까지 log (박스 벽에서 C0 연속)없음
signed-log su = sign(t)·log(1+|t|/s)/log(1+R/s), t = x − c없음

depth 대조군도 같은 계열의 1차원 판(DepthLinear / DepthLinLog)을 만들어 matched mask를 유지했다. linlog에서는 두 arm 모두 validity가 센서 유효성뿐이다.

측정 3단

① 양자화만 tmp/compare_encodings.py CLVR 6 scene × 3 frame × 3 view = 3.1M 픽셀의 실제 world XYZ, encode→decode 오차 박스 안(workspace) / 밖(far field) 분리 ② VAE 통과 tmp/vae_roundtrip_enc.py 실제 256×320 학습 그리드로 리사이즈(로더와 동일 NEAREST_EXACT + center-crop 0.95) → Wan2.2 VAE encode→decode. 기준을 decode(px_grid)로 잡아 양자화 제외한 순수 VAE 비용만 ※ squash는 VAE에게 "far field까지 chroma가 꽉 찬 이미지"를 준다 → 더 싫어할 가능성 확인 필수 ③ 실제 빌드 tmp/build_droid_xwam.py --encoding {box,linlog} 미니셋 2 에피소드씩 생성 후 mp4 왕복 · matched mask 확인

3 결과

박스가 버리는 양 — 센서가 아니라 박스가 주범

sensor-valid 91.52% (depth ∈ (0.05, 6) m) sensor-valid & in-box 52.85% → 박스 단독으로 38.66%p 추가 폐기

① 양자화 오차 (3.1M 픽셀, 3D 유클리드 mm)

스킴유지(sensor-valid 중)sentinel(전체 중)workspace med/p90/p99far-field med/p90
box57.75%47.15%2.97 / 3.95 / 4.58표현 불가
linlog f=0.9100%8.48%3.30 / 4.39 / 5.1076.5 / 294.4
linlog f=0.8100%8.48%3.72 / 4.94 / 5.7537.6 / 131.6
linlog f=0.7100%8.48%4.25 / 5.65 / 6.5625.0 / 85.9
signed-log s=0.3100%8.48%6.73 / 10.42 / 13.3515.6 / 37.6
signed-log s=1100%8.48%11.15 / 14.86 / 17.5016.0 / 30.3

해상도 배분 (mm/level, 축 최대)

스킴박스 중심박스 모서리+0.8m 밖+3.6m 밖
box6.36.3
linlog f=0.97.083.6330.4781.8
linlog f=0.87.959.6140.4369.9
linlog f=0.79.037.795.3242.1
signed-log s=0.37.929.450.9103.0
뷰별 sentinel(전체 픽셀 대비)box: ext1 54.8% / ext2 63.7% / wrist 22.9%. linlog f=0.8: 5.5% / 7.2% / 12.7%. 방을 보는 exterior 두 뷰가 특히 심하게 잘려 있었다. wrist는 반대로 squash에서 살짝 늘지만 절대량이 작다.

② VAE 통과 후 (3 scene × 9 frame, 양자화 제외 순수 VAE 비용, mm)

스킴workspace med/p90/p99far-field med/p90sentinel 유지valid 오염
box8.2 / 33.1 / 207.998.6%0.18%
linlog f=0.98.2 / 33.7 / 201.2246.1 / 1357.893.7%0.17%
linlog f=0.88.6 / 34.0 / 193.4117.1 / 693.893.7%0.06%
linlog f=0.79.1 / 35.2 / 192.174.2 / 477.593.8%0.04%
signed-log s=0.312.4 / 44.1 / 188.736.6 / 252.793.6%0.03%
VAE는 squash를 전혀 벌하지 않는다 — workspace 8.2 vs 8.6mm. "chroma가 꽉 찬 이미지를 더 싫어할 것"이라는 우려는 기각. valid→invalid 오염은 오히려 squash가 3배 낮다(0.18% → 0.06%).

③ 실제 빌드 검증 (미니 2 에피소드)

box sentinel pm 50.69% depth 50.69% mismatch 0 linlog sentinel pm 9.04% depth 9.04% mismatch 0 (scheme=linlog f=0.8) pointmap mp4 round-trip max |Δpx| = 0 (양쪽 모두 무손실 유지)

matched mask는 두 인코딩 모두에서 정확히 0 mismatch로 성립한다.

오차 예산 정리 (workspace, mm)

boxlinlog f=0.8
양자화3.03.7
VAE8.28.6
모델 예측 오차 (260701 실측)~50~50
sentinel 낭비47%8.5%

4 Takeaway

의미

1. 하드 박스는 비용 대비 이득이 없다. workspace 정확도에서 box가 사는 것은 양자화 0.7mm, VAE 포함해도 0.4mm다. 지배항인 모델 예측 오차 ~50mm의 1.5% 미만이다. 그 대가로 픽셀의 38.7%p를 버리고 있었다.

2. linlog f=0.8 채택. 100% 커버리지, sentinel 47%→8.5%, workspace 손실 무시 가능, far field를 실제로 표현. f=0.9는 workspace가 box와 동일하지만 far field가 2배 뭉개지고, signed-log는 workspace를 2~4배 희생한다 — 둘 다 중간이 낫다.

3. depth loss에 mask가 없다는 baseline 결함은 그대로다. sentinel이 8.5%로 줄면 그 결함이 실효적으로 무해해지지만, 애초에 그 결함의 크기를 §1에서 과대평가했다 — §5의 정정을 함께 볼 것. 채택 근거는 "용량 회수"가 아니라 "box가 cross-view 공유 콘텐츠를 지운다"는 쪽이다.

260805 판단의 누락을 정정. 당시 "박스를 XL로 키우면 양자화가 19.5mm로 depth보다 나빠진다"며 박스 확대를 기각했는데, 그 판단은 균일 양자화를 전제한 것이었다. 비균일(단조) 배분이라는 선택지를 고려하지 않은 것이 누락이었고, 그것이 커버리지와 workspace 정밀도 두 목표를 동시에 만족시킨다.
논문과의 관계. See like a Robot은 pointmap을 입력으로 쓰는 축이라 우리(생성 타깃)와 경쟁하지 않는다. 단 "pointmap이 VLA SR을 산다"는 이미 찍혔다(π0.5 RoboCasa 55.3→62.9, 실로봇 미지 카메라 55.0→66.7, DP3 48.3 대비). → 우리 A/B의 질문은 "world model이 상상하는 기하의 표현이 control을 사는가"로 좁혀야 차별화된다 — 260805 REPA 계획의 축과 동일하다. 논문 §4.1(pre-computed transform 34.7 > learned 31.6)은 world-frame 선택의 외부 근거로 인용 가능.

5 자기 정정 — masked loss 검토와 논거 교체

뷰어를 본 사용자가 세 가지를 지적했다. ⓐ "뒤에 쓸데없는 배경들이긴 하네, box가 제일 좋은 것 같기도" ⓑ "배경 부분은 loss가 조금 흐르게 할 수 없나" ⓒ "mask가 학습 때만 있고 infer 때는 없는데, 그럼 infer 때 전체를 denoising 해야 하잖아?"

ⓐ 배경이 정말 배경인가 — 관찰이 수치로 확인됨

박스 벽에서 거리out-of-box 중전체 픽셀 중
~0.2m (경계 근처, 유용할 수도)6.5%2.8%
0.2~0.5m22.2%9.4%
0.5~1.0m39.8%16.8%
1.0~2.0m10.1%4.3%
2.0m 초과21.4%9.0%

median 0.75m, p75 1.40m, p95 5.00m. → 경계 살짝 밖의 "유용할 수도 있는" 부분은 2.8%뿐이고 대부분 진짜 벽·바닥이다. 관찰이 맞다.

ⓑ masked depth loss — 구현은 가능하다

runners/xwam_runner.py:234 depth_loss = (depth_latents_pred[0] - gt_depth_latents).pow(2).mean() ← mask 없음 runners/xwam_runner.py:230 video_loss = ((...) * video_mask).pow(2).sum() / (video_mask.sum()+1e-8) ← 패턴이 바로 옆에 batch["depths"] (픽셀 공간)이 같은 함수에서 접근 가능 → sentinel mask 런타임 계산 가능 단 loss가 latent 공간 → 16×16 px/cell로 풀링 필요. 실측 셀 분포: 완전 sentinel 35.9% / 부분 유효 18.1% / 완전 유효 46.0% (평균 셀 유효율 54.8%)
⚠️ 자기 정정 — "sentinel 47%가 depth 용량 절반을 먹는다"는 과장이었다. 260805에서 물려받아 이 문서 §1에도 그대로 쓴 주장인데, 따져보면 약하다. sentinel은 loss의 을 47% 차지할 뿐 난이도를 차지하지 않는다. 상수 예측은 몇천 step이면 끝나고 그 뒤엔 gradient가 사실상 0이다. 즉 masked loss가 "회수"한다는 36%는 대부분 이미 공짜였다. → 안 C(box + masked loss)의 매력이 이 정정으로 거의 사라진다.

ⓒ train/infer 불일치 — C를 기각하는 근거

학습 중에는 xt = (1−t)·x0 + t·noiseGT에서 만든 노이즈 latent가 attention 입력이 된다. 즉 감독 안 받는 영역도 학습 때는 늘 "진짜 배경"을 보여준다. 추론 시엔 그 영역이 모델 자기 출력으로 굴러가므로 분포가 어긋나고, full self-attention을 통해 workspace 토큰까지 오염된다. 완화 요인은 하나 — X-WAM은 정책 추론에서 run_depth=False(policy_server.py:206)라 SR엔 무관하고, 문제는 "상상 geometry" 직접 평가 지표에서만 터진다. 그래도 새 리스크를 만드는 설계임은 분명하다.

✅ 결론 — linlog 유지, 단 지지 논거를 교체

1. box는 검증 대상 자체를 깎는다. box의 뷰별 sentinel은 ext1 54.8% / ext2 63.7% / wrist 22.9%다. pointmap 가설의 핵심은 "3뷰가 하나의 좌표계를 공유한다"인데, 두 exterior가 공유하는 내용의 상당 부분이 방이다. box는 cross-view 공유 콘텐츠를 절반 이상 지워 pointmap 가설에 불리하게 편향된다. 이 논거는 용량 주장과 독립이다.

2. linlog는 새 하이퍼파라미터도, 새 리스크도 안 만든다. loss를 건드리지 않으므로 X-WAM 목적함수가 그대로고(릴리스 baseline 이탈 없음), 모든 토큰에 타깃이 있으므로 ⓒ의 분포 어긋남이 아예 없다. 대가는 workspace 3.0→3.7mm(+VAE 8.2→8.6mm)로 모델 오차 ~50mm의 1.5% 미만.

D(linlog + 배경 down-weight)는 보류 옵션. 스모크에서 배경이 depth_loss를 지배하면 도입한다. 방식은 soft weight + ε 바닥(셀 유효율을 가중치로, 완전 sentinel 셀은 ε=0.05) — 가중치를 정확히 0으로 두면 그 셀이 무제약이 되어 ⓒ의 문제를 그대로 불러온다.

6 4-way 스모크 — 동일 데이터 100 step

{box, linlog} × {depth, pointmap} 4 arm을 로그인 GPU 1장씩 100 step. 교란 제거: 1차 실행은 box쌍이 기존 10-에피소드 미니셋이라 데이터가 달랐다. 빌더의 episode id가 전역 인덱스라 --episodes 12 --encoding box로 재빌드하면 같은 scene이 선택되므로, box쌍만 재실행해 동일 12 에피소드 / 2,732 clips로 맞췄다.

armvideo_lossdepth_losspm/depth
box + depth0.5110.317
box + pointmap0.5090.5941.87×
linlog + depth0.5150.529
linlog + pointmap0.5080.6101.15×

동일 데이터 pixel-invalid: box 56.55% / linlog 8.54%. 4 arm 모두 완주, Traceback 0, NaN 0. (step≥49 평균 — batch=1이라 단일 step은 노이즈가 크다.)

① A/B 격리 재확인: video_loss가 4 arm 모두 0.508~0.515로 동일하다. 인코딩을 바꿔도 RGB 경로는 불변이고, 차이는 depth 슬롯에만 갇혀 있다.
② 260805가 남긴 교란요인이 해소된다. depth_loss_weight=1.0이 두 arm에 동일한데 pm arm만 depth gradient를 더 받아 총손실 구성이 달라지던 문제: box 1.87× → linlog 1.15×. "pm weight 0.55를 3번째 arm으로 추가"하기로 남겨둔 보완책이 대체로 불필요해진다.
③ 메커니즘 — linlog는 pointmap을 쉽게 만든 게 아니라 depth 대조군을 정직하게 만든다. pointmap의 depth_loss는 0.594 → 0.610으로 거의 안 변한다. 변한 건 depth arm이 0.317 → 0.529로 어려워진 것이다. box에서 depth는 "56.55%가 상수 + 나머지는 좁은 범위 무채색"이라 지나치게 쉬웠고, 대조군이 실제보다 낮은 loss를 내며 격차를 벌리고 있었다. → 인코딩 간 depth_loss 절대값은 비교 불가(box 0.317이 좋아 보이지만 더 쉬운 문제를 푼 것뿐)이며, 비교 가능한 것은 arm 간 비율이다.

함정 기록

로그 파싱 : 학습 로그 앞부분에 수 KB짜리 newly-initialized weight 목록이 있고, Lightning 진행바가 \r로 덮어써 한 step이 거대한 한 "줄" 안에 들어간다. \r로도 split해야 "[METRICS] Step: NNNN - ... train/depth_loss: X"를 뽑을 수 있다. dataloader: 무손실 libx264rgb 미니셋은 디코드가 무겁다. 4 run × 4 worker면 로그인 NAS 경로에서 dataloader bound가 되어 GPU util 0%로 수 분을 보낸다. → 본 학습은 14 CPU/GPU (=112) 로 잡을 것.

7 Next Steps

즉시

본 데이터셋 재빌드--encoding linlog로 CLVR 2,469 scene 재생성(job 71298과 동일하게 14 shard, ~45분, ~69GB). 기존 sft_datasets/DROID_CLVR{,_pm}는 box판이므로 디렉토리 분리.

미측정 — 다음 실험

squash가 diffusion 학습 난이도에 주는 영향. far field까지 실제 구조를 예측해야 하므로 depth_loss 절대값이 달라질 수 있다. 100 step 스모크 ×2를 linlog로 재실행해 260805의 depth_loss 비율(depth 0.459 vs pm 0.840, 1.8×)이 어떻게 변하는지 확인 필요.

채택하지 않음 (현 시점)

EE-frame 재중심화 — 논문 §4.3의 SR 이득(34.7→36.9, 카메라 랜덤화 시 32.7→36.6)은 크지만 P − t_EE강체 평행이동이라 invalid 문제와 무관하고(장면 extent 불변, EE가 움직여 오히려 박스가 커짐), action/proprio가 robot-base인 우리 설계와 좌표계가 어긋난다. 별도 3번째 arm 후보로 분리.

✅ 학습 스크립트 교정 완료

scripts/train_droid_ab.sh 전면 재작성. ① plain pythontorchrun --nproc_per_node=8 — 260805판 헤더의 "torchrun이 TCPStore hang을 유발" 주장은 CLAUDE.md §5(중첩 sbatch의 torchelastic env 누수)와 혼동한 것이라 주석에 정정. 그대로 뒀다면 template.sh의 #SBATCH -n 1 × Lightning SLURM 감지로 260806 단일 GPU 사고(~126 GPU-hour)가 재발한다. ② eff batch 32 → 128 / lr 3e-5(gate_base.sh와 동일 레시피, arm A가 61.0%로 통과한 조건). ③ expect_world_size 가드 · save_top_k=0 · -c 112 · exp_name에 인코딩 태그(box/linlog 체크포인트 충돌 방지) · AB_DATASET_PREFIX로 인코딩 전환.

conda activate xwam AB_GPUS=8 sbmr 20 "bash scripts/train_droid_ab.sh depth" --qos=extra --gres=gpu:8 \ -c 112 --mem 1600GB --time 24:00:00 -J droid_ab_depth AB_GPUS=8 sbmr 20 "bash scripts/train_droid_ab.sh pointmap" --qos=extra --gres=gpu:8 \ -c 112 --mem 1600GB --time 24:00:00 -J droid_ab_pm

남은 블로커 = linlog 본 데이터셋 미생성.

산출물

tmp/encode_schemes.py 스킴 정의 (BoxLinear / LinLog / SignedLog + depth 1D판) tmp/pm_scenes.py DROID scene → world XYZ + 센서 마스크 + RGB 공용 로더 tmp/compare_encodings.py 양자화·커버리지·해상도 배분 비교 tmp/vae_roundtrip_enc.py Wan2.2 VAE 통과 후 비교 tmp/precompute_encoding_clouds.py 3D 뷰어용 사전계산 (encode / +VAE 두 단계) tmp/viz_encodings.py viser 뷰어 — 포트 8072 tmp/build_droid_xwam.py --encoding {box,linlog} 추가, json에 스킴 기록

상세 로그: claude/260807/analysis-pointmap_encoding.md