Index
2026-08-04 — Experiment

M1/M2 Ablation 투입 — 클러스터에 올리기까지 잡은 7건

MetaView → WAM Backbone | Phase 1 · Task 7–8

TL;DR

7
결함
47
Tests
0.0
check_noop
4.1s
M1 step
76.7
M2 GB
1
bad clip

1배경 — 코드는 끝났고, 문제는 클러스터였다

이전 리포트에서 코드 Task 1–6이 완료됐고 Task 7(M1 학습)이 스모크 디버깅 중이었다. 이 리포트는 그 이후 — 두 arm을 실제로 8-GPU 클러스터에 올리는 과정을 다룬다.

목표는 단순했다. M1(branch off)과 M2(branch on)을 동일 조건으로 던지고 wrist LPIPS로 판정한다. 실제로는 안착까지 7건의 결함을 통과해야 했다.

이 프로젝트의 실패 양식은 크래시가 아니다. 7건 중 4건은 에러도 로그도 없이 실험을 무의미하게 만들 종류였다. 크래시한 3건이 오히려 싸게 끝났다.

2결함 1–3 — 하나의 근본 원인, 다섯 개의 경계

스모크 run 4가 camera_token.py:42에서 죽었다: TypeError: Got unsupported ScalarType BFloat16. numpy에 bfloat16이 없다.

구현자가 라이브러리 소스까지 추적해 네 번의 dtype 실패 전체의 단일 근본 원인을 찾았다:

lightning.pytorch.plugins.precision.deepspeed.DeepSpeedPrecision.convert_input → apply_to_collection(batch, _convert_fp_tensor, dst_type=bf16) validation_step 본문이 실행되기 전에 batch dict 전체를 재귀적으로 캐스팅한다.

X-WAM 자체 코드는 이걸 알고 곳곳에서 방어하고 있었다 (gt_actions.float(), VAE의 명시적 .float(), _forward_single.float() 반환). 새로 쓴 코드 두 곳이 그러지 않았을 뿐이다.

M2 최초 실행 — 비대칭이 곧 버그였다

use_geometry=true최초 런타임 실행geometry_branch.py:112에서 죽었다.

q = self.to_q(self.norm_in(x[:, idx])) # 입력: 처리 없음 ← 크래시 ... out[:, idx] = self.to_out(attn).to(out.dtype) # 출력: 주석까지 달아 처리됨 RuntimeError: mat1 and mat2 must have the same dtype, but got Float and BFloat16

x는 X-WAM이 의도적으로 fp32로 유지하는 residual stream이고 (wan_model.py:244-250이 residual add를 명시적 fp32 autocast로 감쌈), to_q.weight는 DeepSpeed가 bf16으로 바꿔놓았다. GeometryBranch는 학습 대상 자식 모듈이라 LPIPS와 같은 sweep에 쓸려갔다.

수정은 X-WAM 자체 관례를 따랐다 — wan_model.py:116x = x.type_as(self.q.weight). 정규화는 들어온 dtype에서, Linear 피연산자만 내려서 캐스팅하는 배치까지 동일하게. 주변 코드와 같은 방식으로 읽힌다.

가장 중요한 방법론적 발견

check_noop은 실제 5B 체크포인트에서 비트 일치 0.0을 냈고 positive control(1.448e-02)까지 있었는데도 이 결함을 놓쳤다. plain torch에서 돌기 때문에 branch 파라미터가 fp32로 남아 x와 우연히 맞는다. 실제 학습이 쓰는 DeepSpeed 정밀도 체제를 한 번도 통과한 적이 없었다.

positive control은 "배선됐는가"를 해결했지 "진짜 정밀도 체제에서 도는가"는 답하지 않는다. 이 프로젝트의 모든 "0이면 통과" 검사에 붙어야 할 단서다.

PSNR/SSIM bf16 영향 — 추정 대신 실측

크래시가 드러낸 두 번째 결함: _psnr/_ssim이 bf16을 그대로 받는데 바로 아래 LPIPS만 .float()을 받고 있었다. M1 vs M2를 판정할 수치라 실측을 지시했다.

조건PSNR 차이판정
양자화 한계 (pred == gt)4.2 dB (61.4 vs 65.6)도달 불가능한 지점
실제 생성 품질 30 / 25 / 20 dB0.0038 / 0.0018 / 0.0015 dB기준(0.01 dB) 아래
프레이밍이 결론을 갈랐다. "4.2 dB"만 인용했다면 근거 없이 심각해 보였을 것이다. 실제 작동 구간을 물었기 때문에 cosmetic으로 판정할 수 있었다. 고친 건 맞는 판단 — 가설이 아니라 실제로 틀린 코드였다.

3메커니즘 검증 — 3중

① 실제 학습 체제 최강

M2 step-0 검증값이 M1과 모든 자리까지 동일:

psnr = 15.27274751663208 lpips = 0.6834847927093506 rot_deg= 80.71382663625363

② check_noop

video / proprio 모두 0.000e+00

positive control 1.448e-02 — dtype 수정 후에도 불변

③ 변이 검증

입력 dtype 경계를 되돌리면 신규 테스트 3개가 실전과 동일한 에러로 실패, 복구하면 통과

①이 check_noop이 구조적으로 보여줄 수 없는 것을 보여준다 — zero-init branch가 진짜 DeepSpeed 안에서 완전한 no-op이라는 사실.

48-GPU 실측

M1M2비고
스텝 시간4.1초 (0.24–0.26 it/s)~5.6초 (0.177 it/s)M2가 1.28× 느림
peak memory30.2 GB76.7 GB183 GB 대비 2.4배 여유
체크포인트~45 GB~50 GB1-GPU 스모크 추정의 8배
8,000 스텝 예상~9.8시간~12.4시간

M1의 4.1초/스텝은 로그 타임스탬프에서 뽑은 사전 추정과 정확히 일치했다.

체크포인트 크기 추정이 8배 틀렸다. 5.7 GB는 1-GPU 스모크 값이고, 8-GPU DeepSpeed는 rank별 optimizer state를 쓴다. save_top_k=-1을 뒀다면 arm당 456 GB가 아니라 3.6 TB였다. 옳은 결정이었지만 근거가 틀렸다.

학습이 실제로 된다

M1이 preempt 전 14분 동안:

stepwrist PSNRwrist LPIPSpose trans_l2
015.270.68351.671
6216.280.63301.112
12516.150.57071.128

판정 지표(wrist LPIPS)가 단조 개선된다 — 파이프라인이 살아있다는 가장 강한 증거.

5결함 5 — 존재하지 않는 MP4 하나가 8-GPU job을 무너뜨렸다

job 69532, step 179 / 8000 droid_raw/TRI/failure/2023-08-10/Thu_Aug_10_10:14:02_2023/recordings/MP4/20252535.mp4 : failed to read frame 10

에러 메시지보다 나쁜 원인: wrist MP4가 아예 없다. cv2.VideoCapture는 없는 경로에 예외도 안 던지고 보고도 안 하고, 열리지 않은 capture를 반환한다. 그래서 파일 부재와 스트림 손상이 동일한 메시지로 나타난다.

왜 아무도 못 잡았나 — 두 가지가 겹쳤다

① 토큰 캐시는 exo 첫 프레임만 읽는다. Task 4가 10,000 clip을 전부 훑었지만 뒤쪽 프레임이나 wrist 스트림은 한 번도 연 적이 없다.
② Task 2의 조인 검사가 표본이었다. "교집합 100% (표본 300)"을 10,000으로 일반화했고, 이게 정확히 그 표본이 놓친 clip이다. 표본에 대한 증거를 집합에 대한 증거로 읽지 말 것.

전수 검사

src/wam/validate_subset.py__getitem__을 그대로 미러링한다 — 같은 serial 해석, 같은 frame_start, 같은 9프레임 윈도우, 3개 뷰 전부. 이게 통과시키면 학습이 읽을 수 있다. try/except 없이 VideoCapture의 반환 코드로 분기하고 사유 문자열을 돌려준다.

검사량
10,000 × 3 × 9
소요
29분 / 30 워커
거부
1 (0.01%)
clean subset
9,999

에폭 수 변화 없음(~51.5), 토큰 캐시는 상위집합이 되어 조회에 문제없음.

6클러스터 운영에서 배운 것

preempt 노출 — 추측 대신 sacct

M1이 14분 만에 preempt됐다. 1 GPU짜리 own job 3개가 우리 8 GPU를 밀어냈다.

≥8 GPU extra, 3일
preempt 비율37 / 58 = 64%
생존 중앙값1.2시간 (p25 0.3h, p75 2.9h)
35분 이내 사망13 / 37
save_interval=500이면 M1의 첫 체크포인트가 34분 → 시도의 35%가 아무것도 남기지 못한다. 100으로 낮춰 6.8분(M1) / 12분(M2)에 첫 체크포인트가 떨어지게 하고, sbmr 재시도를 5 → 30으로 올렸다.

State=COMPLETED를 성공으로 읽으면 안 된다

~/template.sheval -- "$1"을 exit status 검사 없이 실행해 내부가 뭘로 죽든 스크립트는 0으로 끝난다. 크래시한 job 69510·69511·69532가 전부 COMPLETED로 기록됐다. 로그를 봐야 한다.

--gres=gpu:0은 GPU 축만 비운다

CPU 전용 검증 job이 QOSMaxCpuPerUserLimit에 걸렸다. core-own은 사용자당 CPU도 112개로 제한하고, 다른 프로젝트 job이 96개를 쓰고 있었다. CPU 전용 job이 대기하면 CPU 축을 봐야 한다.

7결함 4·7 — 내가 만든 것들

결함 4: 확인하지 않은 값 하나로 두 job이 죽었다

두 arm이 각각 2분 56초, 1분 48초 만에 죽었다. ModelCheckpoint(save_top_k=2, monitor=None) — Lightning은 monitor 없이는 -1/0/1만 받고 __init__에서 거부한다. GPU도 체크포인트도 대기열도 필요 없는 오류였다.

값만 고치지 않았다. 진짜 결함은 값이 아니라 그게 클러스터까지 도달할 수 있었다는 것이라, 콜백 생성을 build_callbacks(config)로 분리해 5B 모델 없이 만들어볼 수 있게 하고 tests/test_train_config.py가 실제 config 두 개로 실제 객체를 생성하게 했다. 변이 검증: save_top_k=2 복원 시 CPU에서 16초 만에 실패한다.

결함 7: 옛 체크포인트를 조용히 이어받을 뻔했다

clean subset으로 재제출하기 전에 experiments/m1_baselinem2_geometry를 지웠다. 두 디렉터리에 옛 10,000-clip 순서로 학습된 step-100 체크포인트가 남아 있었고 run_wam_m*.sh--resume을 넘긴다. 그대로 던졌다면 새 index가 더 이상 기술하지 않는 데이터에서 에러도 로그도 없이 이어받았다.

불변식은 규율이 아니라 테스트로

M2 config를 sed로 파생시키고 diff로 3줄임을 매번 손으로 확인해왔다. 이제 test_the_two_arms_differ_only_in_the_ablated_fields가 강제한다 — index_path를 clean index로 바꿀 때 이 테스트가 실제로 안전망이 됐다. 한쪽 arm만 고치면 실험이 아무 흔적 없이 무의미해진다.

8부수 성과 — VGGT-Omega 해금

접근 권한은 원래 승인돼 있었다. 막힌 원인은 로그인 노드의 HF 신원이 타인(kinam0252)이었던 것. ~/envs/api_keys.txt의 토큰을 명시 전달하면 본인 계정으로 인증된다.

gated repo 판정은 HfApi.auth_check가 한다. model_info는 권한 없이도 통과하므로 권한 검사가 아니다 — 이전에 "거부"로 보인 건 이 오진과, 해당 repo에 config.json이 없어 발생한 404가 겹친 결과였다.
인코더격자토큰dimclip당 캐시
DA3 (res 448)(19,32)1216614414.9 MB
VGGT-Omega(12,20)4802048~1.97 MB

VGGT 격자가 Wan2.2 VAE latent 격자와 정확히 일치한다. 공동 인코딩도 검증했다 — view A 교체 시 view B 토큰이 28.4% 변하고 결정성 대조는 정확히 0.0이라 그 변화가 진짜 cross-view 결합이다.

캐시가 7.6배 작아 subset 확대가 현실적인 선택지가 됐다 — 100k clip 캐시가 DA3로는 1.5 TB지만 VGGT로는 약 200 GB다. 51.5 에폭이 판정력을 압축한다면 이 경로를 쓸 수 있다.

9현재 상태와 다음

M1
job 69582
M2
job 69583
QOS
core-extra ×30
commit
7b75958

미해결

항목내용판단 시점
체크포인트 쓰기 비용 M1이 0.266 → 0.157 → 0.187 it/s. ConsoleLogger 윈도우가 200스텝이라 step 200 이후 점근값이 곧 상각 비용. 0.19에서 멈추면 ~28% → save_interval 200이 더 나은 거래 재시작 실행 step 400 이후
preempt 완주 가능성 중앙값 1.2h 생존, 시도당 ~1,000 스텝 → 8회 이상 필요. 30회 한도로 덮었으나 실증 필요 진행 중
M2 메모리 76.7 GB 스모크 47.3 GB에서 상승 — "geometry hook이 torch_checkpoint 바깥" 이연 항목이 실현. 들어가지만 여유가 6배 → 2.4배 모니터링
own 전환 다른 프로젝트 job이 core-own 8 GPU 전부 점유 중. 끝나면 preempt 없는 완주가 확실히 낫다 해당 job 종료 시

판정 기준

wrist LPIPS가 결정 지표다. 16개 검증 지점(500스텝 간격)의 궤적으로 판단하며, 단일 시점 비교는 하지 않는다. Phase 0에서 DROID의 PSNR 편차가 0.5 dB 미만이고 SSIM이 순위를 뒤집는 걸 확인했기 때문이다.