gpu-zombie-test/results/comparison.md

285 lines
13 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 시나리오 1 — 5가지 변형 비교 분석 보고서
> **목적**: GPU 좀비/고아/orphaned CUDA context 재현 시도 5가지 변형의 결과 비교
> **결론 미리보기**: orphan CUDA context는 **모든 변형에서 미발생** (driver/cgroup이 자동 회수). 진짜 문제는 ① 30초 grace period 낭비 ② NCCL hang으로 인한 GPU 점유.
---
## 1. 5개 변형 한눈에 보기
| # | 디렉토리 | 노드 | GPU | Driver | 유저 | Worker | 특이사항 |
|---|---|---|---|---|---|---|---|
| 1 | `scenario-1/` | gpu-4 | A100 80GB | **590.48.01** | root | 1개 (torch) | 기본 시나리오 |
| 2 | `scenario-1-driver-580.126.09/` | **v1003** | V100 32GB | **580.126.09** | root | 1개 (raw ctypes) | 구버전 driver 비교 |
| 3 | `scenario-1-user-soo/` | gpu-4 | A100 80GB | 590.48.01 | **UID 1000** | 1개 (torch) | non-root 권한 |
| 4 | `scenario-1-user-soo-10workers/` | gpu-4 | A100 80GB | 590.48.01 | UID 1000 | **10개** (torch) | 다중 worker |
| 5 | `scenario-1-nccl-3gpu/` | gpu-4 | A100 80GB ×3 | 590.48.01 | UID 1000 | **3 NCCL ranks** | 멀티 GPU 분산 |
---
## 2. 환경 비교
| 항목 | #1 root | #2 driver 580 | #3 soo 1w | #4 soo 10w | #5 NCCL 3GPU |
|---|---|---|---|---|---|
| 노드 | gpu-4 | v1003 | gpu-4 | gpu-4 | gpu-4 |
| GPU 모델 | A100 SXM4 80GB | V100 SXM2 32GB | A100 80GB | A100 80GB | A100 80GB |
| GPU 수 (요청) | 1 | 1 | 1 | 1 | **3** |
| Driver | 590.48.01 | **580.126.09** | 590.48.01 | 590.48.01 | 590.48.01 |
| CUDA | 13.1 | 12.4 | 13.1 | 13.1 | 13.1 |
| 컨테이너 유저 | root (UID 0) | root | UID 1000 | UID 1000 | UID 1000 |
| GPU 할당 방식 | torch.randn | **raw ctypes** | torch.randn | torch.randn | torch.randn + NCCL |
| Worker 개수 | 1 | 1 | 1 | **10** | **3** (각 다른 GPU) |
| terminationGracePeriodSeconds | 30 | 30 | 30 | 30 | 30 |
> **#2의 raw ctypes 사용 이유**: V100(CC 7.0)이 현재 이미지의 torch(CC 7.5+ 빌드)와 비호환. CUDA driver API 직접 호출로 우회.
---
## 3. 기본 동작 비교 — Step 2 (worker spawn 후 상태)
| 항목 | #1 | #2 | #3 | #4 | #5 |
|---|---|---|---|---|---|
| PID 1 | sleep | sleep | sleep | sleep | sleep |
| 좀비 발생 (kill 전) | ❌ | ❌ | ❌ | ❌ | ❌ |
| 고아 발생 (PPID=1) | ✅ 1개 | ✅ 1개 | ✅ 1개 | **✅ 10개** | **✅ 3개** |
| GPU 메모리 점유 | 2466 MiB | 2356 MiB | 2466 MiB | **24702 MiB** | **5691 MiB** (1897×3) |
| nvidia-smi PID | 노드 PID | **컨테이너 PID** | 노드 PID | 노드 PID | 노드 PID |
| observe.sh PID 일치 | ❌ 오탐지 | ✅ 일치 | ❌ 오탐지 | ❌ 오탐지 | ❌ 오탐지 |
| /dev/nvidia fd | worker만 | worker만 | worker만 | 10개 모두 | **3개 × 모든 GPU** ⭐ |
### 흥미로운 발견 ①: nvidia-smi PID namespace 차이
- **torch 사용 시**: nvidia-smi가 노드 PID(예: 537847)를 반환 → 컨테이너 안의 `/proc/537847` 못 찾음 → observe.sh가 `ORPHANED` 오탐지
- **raw ctypes 사용 시**: nvidia-smi가 컨테이너 PID(예: 50)를 반환 → 일치 → 정확한 판정
- 원인 추정: torch가 내부적으로 다른 namespace에서 CUDA context를 초기화하는 것으로 보임
### 흥미로운 발견 ②: NCCL의 P2P context
- 3 GPU NCCL 환경에서 각 rank는 **자기 GPU + 다른 모든 GPU의 fd를 보유**
- `/dev/nvidia0 → 95 96 97` 식으로 모든 rank가 모든 device에 fd 등록
- 한 rank 죽어도 다른 rank들이 P2P context를 들고 있어서 **그 GPU에 약간의 메모리 잔존**
---
## 4. ⭐ kill -9 후 동작 비교 (Step 3)
| 항목 | #1 | #2 | #3 | #4 | #5 |
|---|---|---|---|---|---|
| kill 권한 (sudo 필요?) | n/a (root) | n/a (root) | **❌ 불필요** | **❌ 불필요** | **❌ 불필요** |
| kill 후 좀비 | ✅ 1개 | ✅ 2개 (이전 잔존 포함) | ✅ 1개 | **✅ 10개** | ✅ 1개 (rank 0만) |
| GPU 메모리 해제 | **즉시 0** | **즉시 0** | **즉시 0** | **즉시 0** | ⚠️ **부분 해제** |
| GPU 메모리 (kill 후) | 0 MiB | 0 MiB | 0 MiB | 0 MiB | **148 MiB 잔존** (peer ctx) |
| GPU 해제 시간 | <1초 | <1초 | <1초 | <1초 (24GB 한꺼번에) | <1초 |
| **orphan CUDA context** | ** 미발생** | ** 미발생** | ** 미발생** | ** 미발생** | ** 부분 발생 (148 MiB)** |
| 다른 worker 영향 | n/a | n/a | n/a | n/a (독립) | **rank 1, 2 NCCL hang** |
### 핵심 발견: orphan CUDA context는 거의 발생 안 함
- **단일 프로세스 kill**: driver가 즉시 회수 0 MiB
- **10 프로세스 동시 kill**: driver가 일괄 회수 24GB 0 MiB (1초 이내)
- **NCCL 멀티 GPU**: 유일하게 부분 잔존 (148 MiB) peer context가 살아있는 다른 rank에 의존
### Non-root 권한 검증 (#3, #4, #5)
> 사용자 의문: "non-root는 sudo 없이 kill 안 되지 않나?"
**답: 같은 UID끼리는 sudo 없이 kill 가능** (Linux 표준)
- exec UID 1000 worker UID 1000 `kill -9` exit 0
- sudo 필요한 경우: 다른 UID 프로세스를 kill 시도할
---
## 5. ⭐ Pod 삭제 동작 비교 (Step 3-B) — 가장 중요한 비교
worker가 살아있는 상태에서 `kubectl delete pod` 실행한 결과.
| 항목 | #1 | #2 | #3 | #4 | #5 |
|---|---|---|---|---|---|
| Pod 삭제 명령 완료 | 31초 | 32초 | 31초 | 32초 | 32초 |
| GPU 점유 지속 시간 | ~30초 | ~30초 | ~30초 | ~30초 | ~30초 |
| 점유된 GPU 메모리 | 2466 MiB | 2356 MiB | 2466 MiB | **24702 MiB** | **5691 MiB** |
| **GPU·sec 낭비** | 73 GB·sec | 71 GB·sec | 73 GB·sec | **740 GB·sec** | **170 GB·sec** |
| SIGTERM 효과 | 무시 | 무시 | 무시 | 무시 | 무시 |
| SIGKILL GPU 해제 | <1초 | <1초 | <1초 | <1초 (24GB) | <1초 (5.7GB) |
| 최종 GPU 잔존 | 0 MiB | 0 MiB | 0 MiB | 0 MiB | 0 MiB |
| 최종 좀비/고아 | 0 | 0 | 0 | 0 | 0 |
| **노드 재부팅 필요** | | | | | |
### 결정적 패턴 (모든 변형 공통)
```
kubectl delete pod
↓ kubelet SIGTERM → PID 1
worker가 SIG_IGN → 무반응
↓ terminationGracePeriodSeconds = 30s 카운트다운
↓ 30초간 GPU 메모리 점유 지속 ⚠️
↓ SIGKILL → cgroup 전체 강제 종료
↓ <1초 이내 모든 GPU 해제 ✅
```
### 가장 큰 GPU 낭비 (#4)
- 10 worker × 30초 동안 24.7 GB 점유 = **740 GB·초 낭비**
- 분산 학습 환경에서 배포/스케일링마다 발생 누적 영향
---
## 6. ⭐ NCCL 멀티 GPU의 특수 케이스 (#5만 발생)
다른 시나리오와 달리 **두 가지 추가 문제** 발생:
### A. NCCL hang으로 인한 GPU 자원 누수
| 시점 | rank 0 | rank 1 | rank 2 | GPU 0 | GPU 2 | GPU 3 |
|---|---|---|---|---|---|---|
| Step 2 | alive | alive | alive | 1897 | 1897 | 1897 |
| **rank 0 kill 직후** | **dead (좀비)** | alive (hang) | alive (hang) | **148** | **1897** | **1897** |
| 30초 | 좀비 | 여전히 hang | 여전히 hang | 148 | 1897 | 1897 |
- rank 1, 2가 NCCL `all_reduce`에서 무한 hang
- 살아있으니 GPU 메모리 풀음 **5.7 GB 영구 점유 (Pod 삭제 전까지)**
- NCCL timeout 미설정 영원히 자원 낭비
### B. Peer context 잔존 (148 MiB)
- rank 0이 죽었지만 rank 1, 2가 GPU 0에 P2P 통신용 context 보유
- driver가 GPU 0의 메모리 일부(148 MiB) 풀지 못함
- 살아있는 rank들이 죽어야 비로소 해제됨
### NCCL 권장 설정 (시나리오 4 변형으로 검증 필요)
```python
os.environ["NCCL_ASYNC_ERROR_HANDLING"] = "1"
dist.init_process_group(
backend="nccl",
timeout=timedelta(seconds=60) # 60초 후 abort
)
def cleanup(signum, frame):
dist.destroy_process_group() # NCCL 정리
torch.cuda.empty_cache()
sys.exit(0)
signal.signal(signal.SIGTERM, cleanup)
```
---
## 7. Driver 버전 영향 비교 (#1 vs #2)
> 가설: 구버전 driver(580)에서는 orphan context가 발생할 수도 있음
| 항목 | gpu-4 (590.48.01) | v1003 (580.126.09) | 결론 |
|---|---|---|---|
| GPU 할당 | | | 동일 |
| kill GPU 해제 | <1초 | <1초 | **동일** |
| Pod 삭제 시간 | 31초 | 32초 | 동일 (측정 오차) |
| SIGKILL 해제 | <1초 | <1초 | 동일 |
| orphan context | 미발생 | 미발생 | **동일** |
| 노드 재부팅 필요 | | | 동일 |
**결론: Driver 580/590 모두 동일하게 안전.** 과거 driver(390/450 시대)에서 보고되던 orphan context 문제는 **현대 driver에서 사실상 해결됨**.
---
## 8. Worker 개수 영향 (#3 vs #4)
> 가설: worker 많아지면 cleanup이 안 될 수도 있음
| 항목 | 1 worker | 10 workers | 비율 |
|---|---|---|---|
| GPU 점유 | 2466 MiB | 24702 MiB | **10×** |
| 고아 발생 | 1 | 10 | 10× |
| kill 좀비 | 1 | 10 | 10× |
| GPU 해제 시간 | <1초 | <1초 | **동일** |
| Pod 삭제 시간 | 31초 | 32초 | 동일 |
| GPU·sec 낭비 | 73 | 740 | **10×** |
**결론: 선형 스케일.** 개수 증가는 메모리·낭비량을 비례로 증가시킬 , 동작 패턴은 동일. SIGKILL의 cgroup 일괄 종료로 100개여도 1초 이내 해제 가능 (이론).
---
## 9. 권한(root vs non-root) 영향 (#1 vs #3)
> 사용자 의문: "non-root면 kill에 sudo 필요 아닌가?"
| 항목 | root | UID 1000 | 차이 |
|---|---|---|---|
| GPU 할당 | | | 없음 |
| 같은 유저 kill | | ** (sudo 없이)** | 없음 |
| 좀비 발생 | | | 동일 |
| 고아 발생 | | | 동일 |
| Pod 삭제 흐름 | 31초 | 31초 | 동일 |
| GPU 해제 | <1초 | <1초 | 동일 |
| **observe.sh 동작** | 모든 fd 보임 | **자기 UID fd만 보임** | 권한 차이 |
**결론: 보안 best practice인 non-root 적용이 실험 결과에 영향 없음.** , `fuser` `/proc` 접근 다른 UID 정보를 있는 부수 효과 존재.
---
## 10. 종합 결론
### 10-1. orphan CUDA context 재현 — **사실상 실패**
여러 각도로 시도했으나, 현대 stack(Driver 580+, containerd, K8s, cgroup v1)에서는:
- Linux kernel이 process death fd close driver release callback 견고
- cgroup이 모든 컨테이너 프로세스를 일괄 종료 (cgroup 밖으로 새어나감)
- CUDA Driver 580/590이 GPU context를 즉시 회수
- "프로세스 죽었는데 GPU 메모리 잔존" 패턴 미발생
**예외: NCCL multi-GPU의 peer context 부분 잔존 (148 MiB)** 진정한 orphan은 아니고 살아있는 rank들이 보유 중인 자원
### 10-2. 진짜 발견된 문제들
| 문제 | 발생 조건 | 영향 | 해결 방안 |
|---|---|---|---|
| **30초 grace 낭비** | SIGTERM 무시 코드 | GB·sec 낭비 | 시나리오 4 (SIGTERM handler) |
| **좀비 누적** | PID 1 = init 아님 | 프로세스 슬롯 점유 | 시나리오 2 (tini) |
| **NCCL hang** | rank 비정상 종료 | 무한 GPU 점유 | NCCL_ASYNC_ERROR_HANDLING + timeout |
| **Peer context 잔존** | NCCL 멀티 GPU | 작은 누수 (148 MiB) | hang 해결 자동 해소 |
### 10-3. 시나리오 2~4의 진짜 가치
plan.md에서 시나리오 2~4의 목적은 "orphan context 방지"였지만, 실제로는:
- **시나리오 2 (tini)**: 좀비 reaping 좀비 누적 방지 (init protection)
- **시나리오 3 (preStop)**: GPU cleanup 명령을 Pod 삭제 먼저 실행 grace period 활용
- **시나리오 4 (SIGTERM handler)**: **30초 grace 낭비 제거 + NCCL graceful shutdown** 가장 가치
### 10-4. 실험에서 배운 교훈
1. **plan.md 가설을 실험으로 검증해야 함** 예상과 다르게 driver는 동작
2. **진짜 문제는 application 코드에 있음** driver는 죄가 없고, SIGTERM handler 부재가 문제
3. **NCCL 같은 분산 라이브러리는 별도 대응 필요** driver 자동 회수로 되는 유일한 케이스
4. **대안적 분석 도구 필요** observe.sh의 PID namespace 오탐지 노드 레벨 도구(gpu-pod-trace.sh) 정확
---
## 11. 시나리오별 결과 디렉토리
| 디렉토리 | notes.md 내용 | 핵심 발견 |
|---|---|---|
| `scenario-1/` | 기본 시나리오 (root, 1 worker) | orphan ctx 미발생, 30초 grace 낭비 |
| `scenario-1-driver-580.126.09/` | V100 + 구버전 driver | driver 버전 무관 동일 결과 |
| `scenario-1-user-soo/` | UID 1000 비교 | non-root에서도 kill에 sudo 불필요 |
| `scenario-1-user-soo-10workers/` | 10 worker 동시 | 선형 스케일, 740 GB·sec 낭비 |
| `scenario-1-nccl-3gpu/` | NCCL 멀티 GPU | **유일한 부분 누수 (148 MiB) + NCCL hang** |
---
## 12. 다음 단계 제안
### A. 시나리오 4 변형으로 NCCL hang 해결 검증
- SIGTERM handler에서 `dist.destroy_process_group()` 호출
- `NCCL_ASYNC_ERROR_HANDLING=1` + timeout 설정
- rank 죽었을 다른 rank가 timeout abort + cleanup하는지 측정
### B. 시나리오 4 단일 GPU 검증
- 30초 grace 낭비 0초로 단축되는지 측정 (1 worker, 10 workers, )
- GPU 메모리 즉시 반환 타이밍
### C. orphan context 재현이 정말 필요하면
- 호스트에서 직접 띄운 GPU 프로세스 (K8s 우회)
- nvidia-container-toolkit bug 재현 (특정 version)
- 또는 plan.md 가설 자체를 "현대 환경에서 미발생"으로 정정