# MoFlow — edge-index 버그 수정과 샘플링 발산 해결 **한 줄 요약** edge-index reshape 버그로 그래프가 씬을 뒤섞고 있었다(`permute` 로 수정). 그런데 **정상 그래프를 넣자 MoFlow 만 발산**했다 — 학습이 아니라 **샘플링**에서. 노드별 **residual-norm cap** 으로 스텝당 섭동을 제한해 해결했다. 수정 대상: `models/graph_interaction_nba.py`(`_make_batched_edge_index`), `models/graph_interaction_nba_v6.py`(SRA 그래프 출력부). 모든 변경은 **환경변수 토글이고 기본값 off** — 켜지 않으면 원본과 동일하게 동작해 기존 체크포인트·기존(버그) 결과의 재현성이 보존된다. --- ## 1. 버그: edge-index 가 씬을 뒤섞음 ```python batched = single.unsqueeze(0).expand(S, -1, -1) + offsets.view(-1, 1, 1) # [S, 2, E0] return batched.reshape(2, -1) # ← 버그 ``` `[S, 2, E0]` 의 메모리 순서는 `s0_src, s0_dst, s1_src, s1_dst, …` 이다. `reshape(2,-1)` 은 이 버퍼를 **앞/뒤 절반**으로 자르므로 row0 에 씬0 블록, row1 에 씬1 블록이 들어간다 — src/dst 가 아니라 **씬으로 나뉜다**. 실측 (A=11, S=4): | | 같은-씬 edge | 이웃 0인 고아 노드 | 노드 degree | |---|---|---|---| | 버그 | **0 %** | **50 %** | 0 또는 20 (정상 10) | | 수정 | **100 %** | 0 % | 전부 10 | `scores.view(B*K*A, A-1)` 의 top-N 이웃 그룹핑도 버그에서 **33 %** 만 실제 타겟과 일치했고, 수정 후 **100 %** 가 됐다. 즉 이웃 선택 자체도 망가져 있었다. 인덱스 범위는 정상(0~43)이라 PyG 가 에러를 내지 않는다 → **크래시 없이 조용히 틀린 그래프**를 쓰고 있었고, 그래서 오래 발견되지 않았다. ### 수정 (`SRA_EDGE_FIX=1`) ```python if os.environ.get('SRA_EDGE_FIX', ...): return batched.permute(1, 0, 2).reshape(2, -1) # src/dst 축을 먼저 → 씬 내부 그래프 return batched.reshape(2, -1) # 기본: 원래(버그) 동작 ``` **baseline 은 그래프를 인스턴스화하지 않으므로 이 버그와 무관하다** — 비교의 한쪽 축은 온전하다. 영향을 받은 것은 SRA/ablation 쪽뿐이다. --- ## 2. 증상: 학습은 정상, 샘플링만 발산 edge 를 고치자 MoFlow-NBA 의 eval ADE 가 폭발했다. 게이트·초기화 처방이 연달아 실패한 뒤, train loss 와 eval 을 **시간 정렬**해서 원인을 특정했다: ``` train loss : 96.5 → 35.6 → 20.1 → 18.0 → 16.6 단조 감소 (완전 정상) eval ADE : 1.13 → 1.07 → 1.52 → 1.85 → 3.49 발산 ``` train loss 가 매끄럽게 내려가는 **바로 그 순간** eval 이 터진다. 이 관찰이 처방의 방향을 바꿨다 — 문제는 학습이 아니라 **샘플링**이다. --- ## 3. 원인: train/sample mismatch (MoFlow 고유) - **학습**: 랜덤 timestep 하나에서 그래프가 **한 번** 적용된다 → 섭동이 누적될 경로가 없다. - **샘플링**: MoFlow 의 flow 적분은 10 스텝. 그래프가 **매 스텝** 적용되고 그 출력이 다음 스텝의 입력(`y_abs`)으로 되먹임된다 → 섭동이 **기하급수적으로 누적**된다. 버그판이 안 터진 이유도 이걸로 설명된다: 노드 절반이 고아여서 그래프 섭동이 사실상 절반만 흘렀고, 그만큼 샘플링 누적이 약했다. **버그가 우연히 정규화 역할**을 하고 있었다. MID·LED 는 iterative denoiser 라 그래프를 반복 정제 과정의 일부로 흡수한다 — 그래서 같은 V6 를 써도 발산하지 않는다. (이전에 관찰한 "MoFlow 는 plug-in 모듈에 유독 민감하고 faithful GameFormer/C2F 도 전부 발산"과 정확히 같은 패턴.) 측정: 그래프 residual 노드 norm ≈ **6.3** vs 호스트 임베딩 노드 norm ≈ **16** → 섭동이 임베딩의 **약 40 %**. 학습이 진행되면 이 분포의 꼬리(outlier)가 커지고, 그 outlier 가 샘플링 누적을 촉발한다. --- ## 4. 실패한 처방들과 그 이유 | 처방 | 건드린 대상 | 결과 | |---|---|---| | `SRA_GATE_SCALE=0.1` (게이트 전역 축소) | 학습 | eval 5회차부터 단조 악화 | | `SRA_SOFT_START` (out_proj zero-init + gate bias −4) | 학습 초기값 | eval **3회차** 발산 (오히려 더 빠름) | | 게이트 기본값 유지 | — | eval 4회차 발산 | 셋 다 **학습**을 조정했다. 그런데 학습은 원래 멀쩡했으므로 효과가 없었다. `SRA_SOFT_START` 는 초기 출력을 완전 identity(‖Δ‖=0)로 만들었는데도, 학습 몇 스텝 만에 `out_proj` 가 자라자 똑같이 터졌다 — **초기 충격 가설이 반증된** 셈이다. --- ## 5. 해법: 노드별 residual-norm cap (`SRA_RES_CAP`) 샘플링 시 **스텝당 노드 섭동의 크기**를 직접 제한한다. 전역 축소(gate scale)와 달리 정상 크기의 섭동은 그대로 두고 **누적을 유발하는 큰 outlier 만** 잘라낸다. ```python res = _gs * gate * self.out_proj(nodes) # [N, D] 그래프 섭동 _cap = float(os.environ.get('SRA_RES_CAP', 0.0)) # 0 = 끔(기본) if _cap > 0: rn = res.norm(dim=-1, keepdim=True) # [N, 1] 노드별 norm res = res * (rn.clamp(max=_cap) / (rn + 1e-6)) # norm 상한 out = orig + res ``` 검증: `SRA_RES_CAP=3.0` 에서 노드 norm 이 정확히 `≤3.0` 으로 잘리고, gradient 는 유지되며(학습 계속 가능), 가변 A(SDD) 도 통과. **gate scale 과의 결정적 차이**: gate scale 은 신호를 영구히 줄여 그래프 효과까지 같이 죽인다. cap 은 분포의 꼬리만 자르므로 그래프의 표현력을 대부분 보존한다. --- ## 6. 결과 ### 발산 지점(eval 3~5) 통과 여부 | eval # | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |---|---|---|---|---|---|---|---|---| | 게이트 기본 | 1.141 | 1.052 | 1.048 | **1.828** | 1.429 | 1.716 | 1.821 | — | | soft-start | 1.134 | 1.075 | **1.516** | 1.852 | 1.697 | 1.862 | 1.765 | — | | gate 0.1 | 1.143 | 1.104 | 1.118 | 1.107 | **1.355** | 1.468 | 1.521 | 1.622 | | **cap 3.0** | 1.145 | 1.053 | 1.007 | 0.969 | 0.942 | 0.988 | 0.927 | **0.894** | | **cap 6.0** | 1.138 | 1.018 | 0.999 | 0.976 | 0.950 | 0.947 | 0.962 | 0.895 | | *baseline(참고)* | 1.163 | 1.021 | 0.961 | 0.964 | 0.952 | 0.955 | 0.953 | 0.895 | cap 두 값 모두 발산 지점을 통과하고 **baseline 궤적을 그대로 따라간다**. ### 장기 수렴 (2026-07-28 기준, 58회 평가 · 10029/25500 = 39 %) | 설정 | 현재 best ADE/FDE | 비고 | |---|---|---| | **cap 6.0** | **0.7373 / 0.9080** | 발산 없이 58회 | | **cap 3.0** | **0.7399 / 0.9300** | 발산 없이 58회 | | *기존(버그판) SRA* | *0.695* | 완주값 | | *baseline* | *0.703* | 완주값 | cap 3.0 과 6.0 의 차이가 **0.0026** 에 불과하다 → 이 처방은 cap 값에 민감하지 않다 (하이퍼파라미터 튜닝에 의존하는 임시방편이 아니라는 근거). ### 다른 설정에서의 재현 같은 처방(`EDGE_FIX + SOFT_START + RES_CAP=3.0`)으로 돌린 MoFlow-NBA ablation 2건도 발산 없이 하강 중 — cap 의 효과가 full SRA 한 설정에만 국한되지 않는다. | 실행 | 평가 수 | 현재 best | |---|---|---| | sparse selection (no σ) | 16 | 0.8034 | | encoding (all nbrs, no σ) | 9 | 0.8648 | --- ## 7. 한계와 미확정 (정직하게) **① SOFT_START 가 교란 변수다.** cap 실행 4건 모두 `SRA_SOFT_START=1` 을 함께 켰다. 따라서 "cap 단독으로 충분한가"는 **아직 검증되지 않았다**. soft-start 는 단독으로는 실패했으므로 cap 이 핵심인 것은 분명하지만, 둘의 기여를 분리하려면 `RES_CAP` 만 켠 대조 실행이 필요하다. **② 최종 수렴값 미확정.** 39 % 지점에서 0.737 이고, 기존(버그판) 0.695 / baseline 0.703 보다 아직 위다. 남은 61 % 와 cosine LR 감쇠에서 더 내려갈 여지는 있으나, **0.695 를 재현하지 못할 가능성도 열려 있다.** **③ 그 경우의 결론도 보고할 가치가 있다.** "정상 씬-내부 그래프는 안정적으로 학습되나 MoFlow 의 one-step flow matching 에서는 이득이 제한적"이라는 것 자체가, 버그판의 0.695 가 **의도한 메커니즘이 아니라 우연한 정규화 효과**였을 수 있음을 시사하는 발견이다. **④ MID / LED 는 다른 처방을 쓴다.** RES_CAP 은 MoFlow 전용이다. MID 는 `graph_gate_init` 0.01→0.001 + warmup 100→200 으로 붕괴를 막았고, LED 는 별도 처방 없이 회복 중이다(LED sparse no-σ 는 이미 기존값 0.886 → **0.800** 으로 앞섬). --- ## 8. 재현 ```bash # MoFlow-NBA full SRA (현재 실행 설정) SRA_EDGE_FIX=1 SRA_SOFT_START=1 SRA_RES_CAP=3.0 \ CUDA_VISIBLE_DEVICES=0 python fm_nba_graph_v6.py \ --cfg cfg/nba/cor_fm.yml --exp edgefix_cap3 \ --batch_size 192 --epochs 150 --fm_in_scaling --tied_noise \ --top_n_neighbors 5 --uncertainty_weight 0.01 --data_dir ./data/nba # no-σ ablation 은 전용 스크립트 사용 (use_sigma_gating=False 하드코딩) SRA_EDGE_FIX=1 SRA_SOFT_START=1 SRA_RES_CAP=3.0 \ python fm_nba_graph_v6_nosigma.py --cfg cfg/nba/cor_fm.yml --exp ef_sparse_nosig \ --batch_size 192 --epochs 150 --fm_in_scaling --tied_noise --top_n_neighbors 5 ``` | 토글 | 기본 | 역할 | |---|---|---| | `SRA_EDGE_FIX` | off | edge-index 씬 혼합 버그 수정 (**핵심**) | | `SRA_RES_CAP` | 0 (off) | 노드별 residual norm 상한 — MoFlow 샘플링 안정화 (**핵심**) | | `SRA_SOFT_START` | off | out_proj zero-init + gate bias −4 (보조, 단독으로는 실패) | | `SRA_GATE_SCALE` | 1.0 | 게이트 전역 축소 (실패한 처방, 코드만 잔존) |