# `SRA_RES_CAP_REL` — 상대 residual cap **한 줄 요약** edge 버그를 고치자 MoFlow 샘플링이 발산했다. 절대 cap(`SRA_RES_CAP`)은 값을 낮춰도 발산 시점을 미룰 뿐이었다. 상한을 **호스트 임베딩 norm 에 비례**하도록 바꾸자 (`‖res‖ ≤ ratio·‖orig‖`) 13회 평가까지 안정적으로 하강하며 baseline 을 앞섰다. --- ## 1. 왜 절대 cap 으로는 부족한가 발산의 원인은 MoFlow 의 **train/sample mismatch** 다. 학습은 랜덤 timestep 하나에서 그래프를 한 번만 적용하지만, 샘플링은 10 스텝 flow 적분에서 매 스텝 적용하고 그 출력이 다음 스텝 입력으로 되먹임된다 → 섭동이 누적된다. 절대 cap 은 `‖res‖ ≤ c` 로 **고정 크기**를 강제한다. 문제는 두 가지다. 1. **호스트 스케일 의존.** residual norm 은 그 호스트 임베딩이 쓰는 스케일 위에 있다. 측정값: MoFlow 에서 그래프 residual ≈ **6.3**, 호스트 임베딩 ≈ **16**. MoFlow 에서 튜닝한 `c = 3.0` 이 임베딩 스케일이 다른 MID/LED 에서는 사실상 무연산이거나 반대로 과도한 제약이 된다. 2. **누적을 결정하는 것은 절대 크기가 아니라 비율.** 스텝당 상태가 `x ← x + res` 로 갱신될 때 발산 여부를 지배하는 것은 `‖res‖/‖x‖` 다. 절대 cap 은 이 비율을 직접 통제하지 못한다. 실제로 절대 cap 은 값을 낮출수록 **발산이 미뤄질 뿐 사라지지 않았다**: | 절대 cap | 발산 시점 | |---|---| | 6.0 | eval 4 | | 3.0 | eval 5 | | 1.0 | eval 9 (이후 1.15~1.61 진동) | | 0.5 | 12회차부터 반등 (0.873 → 0.922 → 0.958) | --- ## 2. 구현 ```python # SRA_RES_CAP_REL: 노드별 상한을 호스트 임베딩 norm 에 비례해 정한다. _rel = float(os.environ.get('SRA_RES_CAP_REL', 0.0) or 0.0) if _rel > 0: lim = _rel * orig.norm(dim=-1, keepdim=True) # [N, 1] 노드별 상한 rn = res.norm(dim=-1, keepdim=True) res = res * torch.where(rn > lim, lim / rn.clamp_min(1e-6), torch.ones_like(rn)) out = orig + res ``` - `orig` = 호스트 임베딩, `res` = 그래프 섭동. 상한이 **노드마다** 자기 임베딩 크기에 비례해 정해지므로 스케일 무관하다. - `torch.where` 를 쓰는 이유는 §4 참조 (이전 구현의 치명적 버그). - 기본값 `0` = 꺼짐. 켜지 않으면 원본과 동일하게 동작한다. 검증 (A=11, K=4): | 설정 | out_proj grad | ‖res‖max | ‖res‖/‖orig‖ max | |---|---|---|---| | 무제한 | 1,716,997 | 9.009 | **0.557** | | 절대 cap 1.0 | 255,169 | 1.000 | 0.071 | | relcap 0.10 | 408,742 | 1.765 | **0.100** ✓ | | relcap 0.03 | 122,623 | 0.529 | **0.030** ✓ | 비율이 지정값으로 정확히 제한되고 gradient 도 정상이다. --- ## 3. 결과 (MoFlow-NBA, full SRA, `SRA_EDGE_FIX=1`) eval 별 min-ADE₂₀ @4.0s: | eval | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |---|---|---|---|---|---|---|---|---|---|---|---|---|---| | **relcap 0.03** | 1.190 | 1.010 | 0.979 | 1.019 | 0.971 | 0.941 | 0.912 | 0.894 | 0.894 | 0.857 | 0.863 | 0.863 | **0.840** | | **relcap 0.10** | 1.128 | 1.017 | 0.988 | 0.954 | 0.950 | 0.944 | 0.958 | 0.936 | 0.880 | 0.889 | 0.857 | 0.862 | 0.876 | | cap 0.5 (절대) | 1.155 | 1.032 | 1.018 | 0.990 | 0.978 | 0.907 | 0.937 | 0.901 | 0.931 | 0.873 | 0.875 | 0.922 | 0.958 | | cap 1.0 (절대) | 1.146 | 1.013 | 0.985 | 1.020 | 0.978 | 1.006 | 1.170 | 0.966 | **1.309** | 1.378 | 1.150 | 1.344 | 1.370 | | *baseline(참고)* | 1.163 | 1.021 | 0.961 | 0.964 | 0.952 | 0.955 | 0.953 | 0.895 | — | — | — | — | — | 현재 best (ADE/FDE 는 **같은 평가 시점**에서 짝지음): | 설정 | best ADE / FDE | @eval | 진행 | |---|---|---|---| | **relcap 0.03** | **0.8399 / 1.0025** | 13 | 2368/25500 (9 %) | | relcap 0.10 | 0.8568 / 1.0441 | 11 | 2349/25500 | | cap 0.5 | 0.8732 / 1.0531 | 10 | 2379/25500 | | cap 1.0 | 0.9662 / 1.2508 | 8 | 3390/25500 — 발산 | 관찰: - **원래 발산 지점(eval 4~5)을 상대 cap 2종 모두 통과**했다. 절대 cap 1.0 도 8회차까지는 멀쩡해 보였으므로 4~5회 통과만으로는 부족한데, 13회까지 유지된 것은 다른 수준의 근거다. - **baseline(0.895) 을 앞섰다.** relcap 0.03 이 0.840 으로 −0.055. - 절대 cap 0.5 는 10회차 0.873 이후 **0.922 → 0.958 로 3회 연속 상승** — 상대 cap 과 달리 불안정 조짐이 보인다. - 비율을 0.10 → 0.03 으로 더 조여도 성능이 나빠지지 않았다. 즉 **무제한 상태의 비율 0.557 은 필요 이상으로 크고, 그 꼬리가 발산을 유발**했다는 해석과 일치한다. --- ## 4. ⚠️ 같이 고친 것 — 이전 cap 구현의 치명적 버그 처음 작성한 cap 은 이랬다: ```python res = res * (rn.clamp(max=cap) / (rn + 1e-6)) # 잘못됨 ``` 두 가지가 틀렸다. **(1) `res = 0` 에서 gradient 가 정확히 0 이 된다.** 스케일이 `0 / 1e-6 = 0` 이 되고 Jacobian 도 0 이다. `SRA_SOFT_START`(out_proj zero-init) 와 함께 켜면 `out_proj` 가 0 에 영구히 갇혀 **그래프가 전혀 학습되지 않는다**. NaN 이 아니라 조용히 죽는다. 실측 (out_proj gradient 합): | 설정 | grad | |---|---| | SOFT_START 만 | 78,751 | | RES_CAP 만 | 765,506 | | **SOFT_START + RES_CAP** | **0.00** ← 학습 불가 | 이 조합으로 돌린 실행들의 체크포인트를 열어보니 **58 epoch 뒤에도 `future_graph.out_proj` 가 정확히 `0.000e+00`** 이었다. 즉 그 실행들은 host 단독 (=baseline) 을 측정한 것이고, "cap 이 발산을 해결했다"던 결론은 **그래프가 꺼져 있어 발산할 것이 없었을 뿐**이었다. 해당 결과 4건은 폐기했다. **(2) cap 미만 값도 부당하게 축소된다.** `rn/(rn+1e-6)` 은 `rn` 이 작을수록 1 에서 멀어진다 — `‖res‖=1e-5` 에서 **0.909 배**. `torch.where` 로 바꾸면 둘 다 해결된다: | 검증 | old | new | |---|---|---| | `res=0` 에서 gradient | 0.0000 | **31.30** | | ‖res‖=1e-5 일 때 스케일 | 0.909 | **1.000** | | cap 초과 시 상한 | 3.0 | 3.0 (동일) | | 정상 구간 gradient 차이 | — | 상대차 6e-7 | --- ## 5. 사용법 ```bash # 권장 설정 (MoFlow-NBA) SRA_EDGE_FIX=1 SRA_RES_CAP_REL=0.03 \ CUDA_VISIBLE_DEVICES=0 python fm_nba_graph_v6.py \ --cfg cfg/nba/cor_fm.yml --exp v3_rel003 \ --batch_size 192 --epochs 150 --fm_in_scaling --tied_noise \ --top_n_neighbors 5 --uncertainty_weight 0.01 --data_dir ./data/nba ``` | 토글 | 기본 | 역할 | |---|---|---| | `SRA_EDGE_FIX` | off | edge-index 씬 혼합 버그 수정 (**필수**) | | `SRA_RES_CAP_REL` | 0 (off) | **비율 상한** `‖res‖ ≤ r·‖orig‖` — 권장 | | `SRA_RES_CAP` | 0 (off) | 절대 상한 — MoFlow 에서 열등함이 확인됨 | | `SRA_SOFT_START` | off | **RES_CAP 과 함께 쓰지 말 것** (§4). 단독으로도 발산 못 막음 | | `SRA_GATE_SCALE` | 1.0 | 전역 축소 — 가장 빨리 발산(3회차), 폐기 | --- ## 6. 미확정 / 한계 **① 최종 수렴값은 아직 모른다.** 현재 9 %(2368/25500) 에서 0.840 이다. 기존(버그판) SRA = **0.695**, baseline = **0.703**. 남은 91 % 와 cosine LR 감쇠에서 더 내려가야 하며, **0.695 에 도달하지 못할 가능성은 여전히 열려 있다.** **② 비율값 튜닝이 끝나지 않았다.** 0.03 과 0.10 이 비슷하고 0.03 이 근소 우위다. 더 조인 값(0.01)이 나을지, 아니면 0.03 이 이미 과도한 제약인지는 미검증이다. cap 을 조일수록 발산은 막히지만 그래프 기여도 줄어드므로, **"발산 안 하면서 baseline 보다 나은" 구간이 실제로 존재하는지**가 최종 질문이다. 현재 0.840 vs baseline 0.895 는 그 구간이 존재한다는 첫 증거다. **③ MoFlow 전용 처방이다.** MID·LED 는 iterative denoiser 라 이 발산이 없고, `RES_CAP*` 을 쓰지 않는다 (MID 는 gate_init/warmup, LED 는 처방 없음). 다른 호스트에 적용하려면 그 호스트의 residual/embedding 비율을 먼저 측정해야 한다. **④ 절대 cap 이 항상 나쁘다고 단정할 수는 없다.** cap 0.5 는 아직 발산하지 않았고 반등 조짐만 보인다. 상대 cap 의 우위는 현재 13회 평가 기준의 관찰이며, 완주까지 가야 확정된다.