Compare commits

...

1 Commits
v0.1.0 ... main

Author SHA1 Message Date
selee fc045680c3 docs: 시나리오 1 5가지 변형 실험 결과 반영
실측 결과 plan.md의 핵심 가설("orphan CUDA context 발생")이 미검증.
대신 두 가지 진짜 문제 발견:
- 30초 grace period 동안 GPU 점유 (SIGTERM 무시 시)
- NCCL multi-GPU에서 한 rank 죽으면 나머지 hang으로 GPU 5.7GB 영구 점유

변경 내용:
- results/comparison.md 신규 — 5개 변형(driver, user, worker개수, NCCL) 비교 분석
- README.md — 실험 결과 요약 섹션, 시나리오 가설들에 검증 결과 추가
- plan.md — Phase 1 직전에 실험 결과 요약 + Step 3-B 절차 추가
                + 시나리오 2~4 가치 재정의

시나리오 2~4는 미실험 — 시나리오 1 결과를 반영해서 갱신된 예상치만 기재.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:58:24 +09:00
3 changed files with 522 additions and 51 deletions

126
README.md
View File

@ -5,7 +5,54 @@ Kubernetes 위에서 GPU 워크로드가 예기치 않게 종료되거나 signal
단계적으로 재현하고, 각 수정안(tini, preStop hook, code-level signal handler)이 단계적으로 재현하고, 각 수정안(tini, preStop hook, code-level signal handler)이
어디까지 문제를 해결하는지 정량적으로 검증하는 실험 프로젝트. 어디까지 문제를 해결하는지 정량적으로 검증하는 실험 프로젝트.
> 실험 설계 전문은 [`plan.md`](./plan.md) 참조. 이 README는 프로젝트 전체 개요와 사용법. > 실험 설계 전문은 [`plan.md`](./plan.md) 참조. 5개 변형 실험 결과 비교는 [`results/comparison.md`](./results/comparison.md).
---
## ⭐ 실험 결과 요약 (2026-04-13~14)
시나리오 1을 5가지 변형으로 실행한 결과 **plan.md의 핵심 가설("orphan CUDA context 발생")이 현대 driver(580/590)에서 검증되지 않음**.
대신 **두 가지 다른 진짜 문제**가 더 중요한 것으로 드러남.
### 핵심 발견
| 가설 (plan.md) | 실제 결과 | 비고 |
|---|---|---|
| `kill -9` 후 orphaned CUDA context 잔존 | **❌ 미발생** | driver가 즉시 회수 |
| Pod 삭제 후 GPU 메모리 잔존 | **❌ 미발생** | cgroup SIGKILL → driver 회수 |
| 노드 재부팅 필요 가능 | **❌ 불필요** | 모든 케이스 자동 정리 |
| 좀비/고아 발생 | ✅ **재현 성공** | tini로 해결됨 (시나리오 2 효과 확인) |
| 30초 grace 동안 GPU 낭비 | **⭐ 발견 (진짜 문제)** | SIGTERM 무시 시 항상 발생 |
| NCCL hang으로 GPU 점유 | **⭐ 발견 (예상 못한 문제)** | 멀티 GPU 환경에서 발생 |
### 진짜 문제 vs plan.md 원래 가설
| 진짜 문제 | 발생 조건 | 영향 | 시나리오 X에서 해결 |
|---|---|---|---|
| **30초 grace 낭비** | SIGTERM 무시 코드 | GB·sec 낭비 (10w일 때 740 GB·sec) | **시나리오 4 (SIGTERM handler)** |
| **좀비 누적** | PID 1 ≠ init | 프로세스 슬롯 점유 | **시나리오 2 (tini)** |
| **NCCL hang** | rank 비정상 종료 | 무한 GPU 점유 | NCCL_ASYNC_ERROR_HANDLING + timeout |
| **Peer ctx 잔존** | NCCL 멀티 GPU | 작은 누수 (148 MiB) | NCCL hang 해결 시 자동 |
### 5개 변형 실험
| # | 변형 | 노드/Driver | 핵심 발견 | 결과 디렉토리 |
|---|---|---|---|---|
| 1 | 기본 (root, 1 worker) | gpu-4 / 590.48.01 | orphan ctx 미발생 | `results/scenario-1/` |
| 2 | 구버전 driver | v1003 / 580.126.09 | driver 버전 무관 동일 | `results/scenario-1-driver-580.126.09/` |
| 3 | non-root (UID 1000) | gpu-4 / 590.48.01 | sudo 없이 kill 가능 | `results/scenario-1-user-soo/` |
| 4 | 10 workers 동시 | gpu-4 / 590.48.01 | 선형 스케일, 740 GB·sec 낭비 | `results/scenario-1-user-soo-10workers/` |
| 5 | NCCL 3 GPU | gpu-4 / 590.48.01 | ⭐ **부분 누수 + hang** (유일) | `results/scenario-1-nccl-3gpu/` |
자세한 비교 분석: [`results/comparison.md`](./results/comparison.md)
### 시나리오 2~4의 재정의된 가치
| 시나리오 | 원래 목적 (plan.md) | 실제 가치 |
|---|---|---|
| 2 (tini) | orphan ctx 방지 | **좀비 reaping** (init protection) |
| 3 (preStop) | orphan ctx 방지 | GPU cleanup 명령 grace 활용 |
| 4 (Full Fix) | orphan ctx 방지 | ⭐ **30초 grace 낭비 제거 + NCCL graceful shutdown** |
--- ---
@ -52,44 +99,62 @@ GPU 메모리는 잡혀 있는 현상(**ghost context**)은 현업에서 자주
### 각 시나리오가 검증하는 가설 ### 각 시나리오가 검증하는 가설
**시나리오 1 — No Fix** **시나리오 1 — No Fix** (실험 완료, 5가지 변형으로 검증)
> PID 1이 bash면 좀비/고아/orphaned CUDA context가 모두 발생한다. > 가설: PID 1이 bash면 좀비/고아/orphaned CUDA context가 모두 발생한다.
- 부모 프로세스가 `wait()` 없이 종료 → 자식(gpu_worker)은 PPID=1 고아 검증 결과:
- bash는 좀비 수거 안 함 → 좀비 잔존 - ✅ 고아 발생 — `parent.py` 종료 후 `gpu_worker.py` PPID=1 reparent
- worker를 `kill -9` → nvidia-smi에서 프로세스 사라져도 GPU 메모리 잔존 (orphaned context) - ✅ 좀비 발생 — kill -9 후 PID 1(sleep)이 reaping 안 함
- Pod 삭제 후에도 GPU 메모리가 잔존할 수 있음 → 노드 재부팅 필요 가능 - ❌ **orphaned CUDA context 미발생** — driver 590/580 모두 즉시 회수
- ❌ **Pod 삭제 후 GPU 잔존 미발생** — cgroup SIGKILL이 항상 정리
- ⭐ **새 발견**: SIGTERM 무시 시 30초 grace 동안 GPU 점유 지속 (배포/스케일 시 리소스 낭비)
- ⭐ **새 발견 (NCCL)**: rank 0 죽으면 rank 1, 2가 hang → GPU 5.7GB 영구 점유 (Pod 삭제 전까지)
**시나리오 2 — tini only** > Tip: PID 1 = `sleep infinity` (bash exec 최적화로 sleep이 PID 1을 대체하지만, init 아닌 동작은 동일)
> tini가 PID 1이면 좀비는 수거되지만, 코드에 signal handler가 없으면 CUDA cleanup은 여전히 실패한다.
- tini의 `waitpid(-1)` 덕분에 좀비는 사라짐 ✅ **시나리오 2 — tini only** (미실험, 가설 갱신)
- 그러나 `kill -TERM`을 worker가 무시하므로 kubectl delete 시 gracePeriod 후 SIGKILL → orphaned context 여전히 발생 > 가설: tini가 PID 1이면 좀비는 수거되지만, 코드에 signal handler가 없으면 CUDA cleanup은 여전히 실패한다.
**시나리오 3 — tini + preStop** 원래 가설:
> preStop hook이 SIGTERM을 보내도, 코드가 SIGTERM을 무시하면 결국 preStop timeout → SIGKILL fallback 경로를 타고 orphaned context는 여전히 발생한다. - tini의 `waitpid(-1)` 덕분에 좀비는 사라짐 → ✅ 예상
- Pod 삭제 → preStop hook 실행 (`/usr/local/bin/prestop.sh`) 갱신된 가치 (시나리오 1 결과 반영):
- prestop.sh가 GPU 점유 PID에 SIGTERM 전송 → 20초 대기 → **worker 무반응** → SIGKILL fallback - **좀비 reaping** — 시나리오 1의 좀비 발생을 막는 init 보호
- 결과적으로 시나리오 2와 거의 동일 (단지 timing만 제어됨) - ❌ **orphan CUDA ctx 방지 가치는 사실상 없음** (시나리오 1에서 driver가 자동 회수)
- ⚠️ **30초 grace 낭비는 그대로** (signal handler 없으므로 동일)
**시나리오 4 — Full Fix** **시나리오 3 — tini + preStop** (미실험, 가설 갱신)
> tini + preStop + 코드 signal handler + 충분한 gracePeriod가 모두 갖춰지면 좀비·고아·orphaned context 모두 없다. > 가설: preStop hook으로 SIGTERM 미리 보내도 worker가 무시하면 SIGKILL fallback 발생.
- worker가 SIGTERM handler에서 `tensors.clear()``torch.cuda.empty_cache()``torch.cuda.synchronize()` → exit 갱신된 가치:
- preStop은 동일한 prestop.sh를 호출하지만 worker가 graceful하게 응답하므로 SIGKILL fallback 경로 미트리거 - preStop이 GPU cleanup 명령(`/usr/local/bin/prestop.sh`)을 grace period 안에 미리 실행
- 다만 worker가 SIGTERM 무시하면 시나리오 2와 동일하게 30초 후 SIGKILL
- ⚠️ **본질적으로 application 코드가 SIGTERM을 처리하지 않으면 효과 제한적**
**시나리오 4 — Full Fix** (미실험, 가장 큰 가치 기대)
> 가설: tini + preStop + 코드 signal handler 모두 적용 시 깨끗한 정리.
갱신된 가치 (가장 중요):
- ⭐ **30초 grace 낭비 → 0초로 단축** (시나리오 1에서 발견된 진짜 문제 해결)
- ⭐ **NCCL `dist.destroy_process_group()` 호출** — multi-GPU에서 5.7GB 누수 방지
- worker가 SIGTERM 받아서 즉시 `tensors.clear()``torch.cuda.empty_cache()` → exit
- 결과적으로 GPU 메모리 빠른 반환 + application graceful shutdown
- Pod 삭제 후 GPU 메모리 완전 해제, ghost context 없음 - Pod 삭제 후 GPU 메모리 완전 해제, ghost context 없음
### 최종 비교표 (예상 결과) ### 최종 비교표 (S1 실측 + S2~4 갱신 예상)
| 항목 | S1 | S2 | S3 | S4 | | 항목 | S1 (실측) | S2 (예상) | S3 (예상) | S4 (예상) |
|---|---|---|---|---| |---|---|---|---|---|
| 좀비 수거 | ❌ | ✅ | ✅ | ✅ | | 좀비 수거 | ❌ 발생 (1~10개) | ✅ tini가 reap | ✅ | ✅ |
| 고아 GPU 점유 | ❌ 잔존 | ❌ 잔존 | ⚠️ SIGKILL fallback | ✅ graceful | | 고아 발생 | ✅ (PPID=1) | ✅ but tini가 reap | ✅ but reap | 미발생 (handler가 정리) |
| SIGTERM → CUDA cleanup | ❌ | ❌ (handler 없음) | ❌ (handler 없음) | ✅ | | SIGTERM 처리 | ❌ SIG_IGN | ❌ SIG_IGN | ❌ but preStop이 cleanup 시도 | ✅ graceful |
| kill -9 후 orphaned ctx | ❌ 발생 | ❌ 발생 | ❌ 발생 가능 | N/A | | kill -9 후 orphaned CUDA ctx | **❌ 미발생** (driver 회수) | 동일 예상 | 동일 예상 | N/A |
| Pod 삭제 후 GPU 메모리 | ❌ 잔존 가능 | ⚠️ 불확실 | ⚠️ 불확실 | ✅ 해제 | | Pod 삭제 시 GPU 점유 시간 | **30초** (grace 전체) | 30초 (handler 없음) | 30초 (worker 무시) | **0초** ⭐ |
| 노드 재부팅 필요 | 가능 | 가능 | 가능 | 불필요 | | Pod 삭제 후 GPU 메모리 | ✅ 해제 (SIGKILL 후) | ✅ 해제 | ✅ 해제 | ✅ 즉시 해제 |
| 노드 재부팅 필요 | ❌ 불필요 | ❌ | ❌ | ❌ |
| **NCCL hang 시 GPU 점유** | **5.7GB 영구 점유** | 동일 (handler 없음) | preStop도 hang 못 풂 | NCCL_ASYNC + destroy_process_group |
> **시나리오 2~4는 미실험.** 시나리오 1 결과를 바탕으로 갱신된 예상치. 실측 시 [`results/`](./results/) 업데이트.
--- ---
@ -232,6 +297,11 @@ bash /path/to/gpu-pod-trace.sh
7. `/dev/nvidia*` fd 보유 프로세스 (`fuser`) 7. `/dev/nvidia*` fd 보유 프로세스 (`fuser`)
8. Orphaned GPU context 종합 체크 (nvidia-smi ∩ fuser ∩ /proc 생존 확인) 8. Orphaned GPU context 종합 체크 (nvidia-smi ∩ fuser ∩ /proc 생존 확인)
> **⚠️ 알려진 한계 (시나리오 1 실험에서 발견)**: torch 사용 시 nvidia-smi가 컨테이너 내부 PID가 아닌
> **노드 PID**를 반환 → observe.sh가 컨테이너 내부 `/proc/<노드 PID>` 조회 실패 → "ORPHANED" 오탐지 발생.
> 정확한 orphaned context 판정은 노드 레벨 `gpu-pod-trace.sh`에서만 가능.
> raw CUDA driver API(ctypes) 사용 시는 컨테이너 PID가 일치해서 오탐지 없음.
### `scripts/node-observe.sh` (gpu-4 노드 root) ### `scripts/node-observe.sh` (gpu-4 노드 root)
노드 레벨에서 GPU 전체 상태 + ghost context 감지. 노드 레벨에서 GPU 전체 상태 + ghost context 감지.

163
plan.md
View File

@ -27,6 +27,101 @@
--- ---
## ⭐ 실험 결과 요약 (2026-04-13~14)
> **이 plan.md는 실험 전 작성됐고, 실제 진행 결과 핵심 가설이 검증되지 않음.**
> 시나리오 2~4 진행 전에 이 섹션을 먼저 읽고 가설을 갱신할 것.
> 자세한 비교 분석: [`results/comparison.md`](./results/comparison.md)
### 시나리오 1을 5가지 변형으로 실행한 결과
| # | 변형 | 결과 디렉토리 |
|---|---|---|
| 1 | 기본 (root, 1 worker, A100) | `results/scenario-1/` |
| 2 | 구버전 driver (V100, Driver 580) | `results/scenario-1-driver-580.126.09/` |
| 3 | non-root (UID 1000) | `results/scenario-1-user-soo/` |
| 4 | 10 workers 동시 | `results/scenario-1-user-soo-10workers/` |
| 5 | NCCL 3 GPU 분산 | `results/scenario-1-nccl-3gpu/` |
### plan.md 가설 vs 실측 결과
| plan.md 가설 | 실측 결과 | 비고 |
|---|---|---|
| `kill -9` 후 orphaned CUDA context 잔존 | **❌ 미발생 (5개 변형 모두)** | Driver 580/590이 즉시 회수 |
| Pod 삭제 후 GPU 메모리 잔존 | **❌ 미발생** | cgroup SIGKILL → driver 회수 |
| 노드 재부팅 필요 가능 | **❌ 불필요** | 모든 케이스 자동 정리 |
| `nvidia-smi --gpu-reset` 필요 | **❌ 불필요** | 동일 |
| 좀비 발생 (PID 1 = init 아님) | ✅ **재현 성공** | sleep도 reaping 안 함 |
| 고아 발생 | ✅ **재현 성공** | parent 종료 후 PPID=1 |
### 새로 발견된 진짜 문제 (plan.md에 없던 것)
| 문제 | 발견 시나리오 | 영향 | 해결 (시나리오 X에서 검증 예정) |
|---|---|---|---|
| **30초 grace period 동안 GPU 점유** | #1 (단일 worker), #4 (10w 740 GB·sec) | 배포 시마다 발생, 누적 영향 큼 | **시나리오 4 (SIGTERM handler)** |
| **NCCL hang으로 영구 GPU 점유** | #5 (멀티 GPU) | 5.7 GB 무한 점유 | NCCL_ASYNC_ERROR_HANDLING + timeout |
| **Peer context 부분 잔존** | #5 NCCL only | 148 MiB 작은 누수 | NCCL hang 해결 시 자동 |
### Step 3-B 추가 (전 시나리오 공통)
> plan.md 원래 절차에는 "kill -9 후 관찰" 만 있었는데, **실제 운영 시나리오는 "worker 살아있는 상태에서 Pod 삭제"**.
> 이 케이스를 모든 시나리오에 추가해야 함.
```bash
# Step 3-B: worker 살아있는 상태에서 Pod 삭제
# 1. parent.py 실행 → worker가 GPU 점유 중 확인
kubectl exec <pod> -- bash -c "cd /app && python3 parent.py & sleep 10 && observe.sh"
# 2. 노드에서 GPU 모니터링 (백그라운드)
ssh ubuntu@gpu-4 'bash /tmp/gpu-monitor.sh' > step3b-monitor.log &
# 3. Pod 삭제 (worker는 살아있고 SIGTERM 무시 상태)
kubectl delete pod <pod> -n gpu-zombie-test
# 4. 모니터 로그 분석 (1초 간격으로 GPU 메모리 변화)
# 예상 패턴: 30초 점유 지속 → SIGKILL → 1초 이내 해제
```
이 절차는 **GPU·sec 낭비량을 정량 측정**하는 핵심 데이터를 제공.
### 시나리오 2~4의 가치 재정의
| 시나리오 | 원래 목적 (plan.md) | 실제 가치 (시나리오 1 결과 후 갱신) |
|---|---|---|
| 2 (tini) | orphan ctx 방지 | **좀비 reaping** (init protection) — orphan ctx와 무관 |
| 3 (preStop) | orphan ctx 방지 | preStop hook으로 grace 안에 cleanup 시도 — worker가 SIGTERM 무시면 효과 제한 |
| 4 (Full Fix) | orphan ctx 방지 | ⭐ **30초 grace 낭비 제거 + NCCL graceful shutdown** ← 가장 큰 가치 |
### NCCL 멀티 GPU의 특수성 (시나리오 4 변형으로 검증 필요)
```python
# 시나리오 4 NCCL 변형에 추가할 코드
import os, signal
from datetime import timedelta
import torch.distributed as dist
os.environ["NCCL_ASYNC_ERROR_HANDLING"] = "1"
dist.init_process_group(
backend="nccl",
timeout=timedelta(seconds=60) # rank 죽으면 60초 후 abort
)
def graceful_shutdown(signum, frame):
print(f"[Worker] Signal {signum} received, NCCL cleanup...")
dist.destroy_process_group() # NCCL 정리 (collective op abort)
torch.cuda.empty_cache()
sys.exit(0)
signal.signal(signal.SIGTERM, graceful_shutdown)
```
### observe.sh의 알려진 한계
torch 사용 시 nvidia-smi가 노드 PID를 반환 → 컨테이너 내부에서 `/proc/<노드 PID>` 조회 실패 → **observe.sh가 "ORPHANED GPU CONTEXT"로 오탐지**.
정확한 판정은 노드 레벨 `gpu-pod-trace.sh`에서만 가능. raw CUDA(ctypes) 사용 시는 PID 일치해서 오탐지 없음.
---
## 사전 준비 ## 사전 준비
### 체크리스트 ### 체크리스트
@ -1043,34 +1138,56 @@ ssh gpu-4
bash gpu-pod-trace.sh bash gpu-pod-trace.sh
``` ```
### 체크리스트 — 시나리오 1 ### 체크리스트 — 시나리오 1 (실측 결과 포함)
**재현 확인:** > ⭐ 2026-04-13~14 실측 완료. ✅ 검증됨, ❌ plan.md 가설 틀림, ⭐ 새 발견. 자세히는 `results/scenario-1/notes.md`.
- [ ] PID 1이 bash인 것 확인
- [ ] parent.py 실행 후 5초 뒤 부모 종료 확인
- [ ] gpu_worker.py가 고아가 됨 확인 (PPID=1)
- [ ] 좀비 프로세스 존재 확인 (`ps aux | awk '$8~/Z/'`)
- [ ] GPU 메모리 점유 확인 (`nvidia-smi`)
- [ ] gpu-pod-trace.sh로 해당 PID → Pod 매핑 확인
**kill -9 후 orphaned context 재현:** **재현 확인 (Step 1+2):**
- [ ] `kill -9 <worker_pid>` 실행 - [x] PID 1 확인 — **`sleep infinity`** (bash exec 최적화로 sleep이 PID 1 대체. init 아닌 동작은 동일)
- [ ] `ls /proc/<worker_pid>` → "No such file or directory" 확인 - [x] parent.py 실행 후 5초 뒤 부모 종료 확인 ✅
- [ ] `nvidia-smi` → GPU 메모리 여전히 점유 확인 → **orphaned context 재현** - [x] gpu_worker.py가 고아가 됨 확인 (PPID=1) ✅
- [ ] `fuser -v /dev/nvidia0` → 해당 PID 상태 확인 - [x] 좀비는 kill -9 후 발생 (parent.py가 즉시 죽으면 좀비 단계 없이 사라짐)
- [ ] gpu-pod-trace.sh 재실행 → PID DEAD 또는 GHOST 상태 확인 - [x] GPU 메모리 점유 확인 (~2466 MiB) ✅
- [x] gpu-pod-trace.sh로 PID → Pod 매핑 확인 ✅
- [⚠️] observe.sh가 "ORPHANED" 오탐지 — torch가 노드 PID 반환 → 컨테이너에서 `/proc/<노드PID>` 미발견. **노드 트레이스로 정확 판정 가능.**
**Pod 삭제 테스트:** **kill -9 후 orphaned context 재현 (Step 3) — ❌ 가설 틀림:**
- [ ] 터미널 1 (gpu-4): `watch -n 1 'nvidia-smi'` - [x] `kill -9 <worker_pid>` 실행
- [ ] 터미널 2: `kubectl delete pod gpu-zombie-nofix -n gpu-zombie-test` - [x] `ls /proc/<worker_pid>` → "No such file" ✅
- [ ] Pod 삭제 후 GPU 메모리 해제 여부 기록 - [❌] **`nvidia-smi` → GPU 메모리 0 MiB (즉시 해제)** ⭐ orphaned context **미발생**
- [ ] 잔존 시 gpu-pod-trace.sh로 ghost context 확인 - [x] `fuser -v /dev/nvidia0` → fd 잡는 프로세스 없음 ✅
- [ ] 결과 저장 (`results/scenario-1/`) - [x] gpu-pod-trace.sh → 죽은 프로세스 0개
**⭐ Step 3-B (worker 살아있는 상태 Pod 삭제) — 새로 추가된 핵심 테스트:**
- [x] parent.py 실행 후 worker GPU 점유 확인
- [x] 노드에서 GPU 1초 모니터링 시작
- [x] `kubectl delete pod ...`
- [x] **30초간 GPU 2466 MiB 점유 지속** (worker가 SIGTERM 무시)
- [x] **31초째 SIGKILL → GPU 1초 이내 해제 (0 MiB)**
- [⭐] **GPU·sec 낭비 = 73 GB·sec (단일 worker)** — 진짜 문제로 식별
**Pod 삭제 테스트 (kill 후) — ❌ 가설 틀림:**
- [x] `nvidia-smi` watch (이미 0 상태)
- [x] `kubectl delete pod ...`
- [x] **Pod 삭제 후 GPU 0 MiB 그대로** ⭐ 잔존 미발생
- [x] ghost context 없음
- [x] 결과 저장 (`results/scenario-1/`) ✅
**다음 시나리오 전 초기화:** **다음 시나리오 전 초기화:**
- [ ] `nvidia-smi`로 GPU 메모리 0 확인 - [x] `nvidia-smi`로 GPU 메모리 0 확인 ✅
- [ ] 잔존 시 `nvidia-smi --gpu-reset` 시도 - [❌] **`nvidia-smi --gpu-reset` 불필요** (가설 틀림)
- [ ] 안 되면 gpu-4 노드 재부팅 → GPU 정상 확인 - [❌] **노드 재부팅 불필요** (가설 틀림)
### ⭐ 추가로 진행한 4가지 변형 (시나리오 1 확장)
```
results/scenario-1-driver-580.126.09/ # V100 + Driver 580 → 동일 결과
results/scenario-1-user-soo/ # UID 1000 → sudo 없이 kill 가능
results/scenario-1-user-soo-10workers/ # 10 workers → 740 GB·sec 낭비
results/scenario-1-nccl-3gpu/ # ⭐ NCCL hang + 148 MiB peer ctx 누수
```
자세한 비교: [`results/comparison.md`](./results/comparison.md)
--- ---

284
results/comparison.md Normal file
View File

@ -0,0 +1,284 @@
# 시나리오 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 가설 자체를 "현대 환경에서 미발생"으로 정정