From fc045680c3faf28e504e1d20e7b7ee42d59b31bf Mon Sep 17 00:00:00 2001 From: selee Date: Wed, 15 Apr 2026 15:58:24 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4=20?= =?UTF-8?q?1=205=EA=B0=80=EC=A7=80=20=EB=B3=80=ED=98=95=20=EC=8B=A4?= =?UTF-8?q?=ED=97=98=20=EA=B2=B0=EA=B3=BC=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 실측 결과 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) --- README.md | 126 ++++++++++++++----- plan.md | 163 ++++++++++++++++++++---- results/comparison.md | 284 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 522 insertions(+), 51 deletions(-) create mode 100644 results/comparison.md diff --git a/README.md b/README.md index e3747ad..61ae2e4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,54 @@ Kubernetes 위에서 GPU 워크로드가 예기치 않게 종료되거나 signal 단계적으로 재현하고, 각 수정안(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** -> PID 1이 bash면 좀비/고아/orphaned CUDA context가 모두 발생한다. +**시나리오 1 — No Fix** (실험 완료, 5가지 변형으로 검증) +> 가설: PID 1이 bash면 좀비/고아/orphaned CUDA context가 모두 발생한다. -- 부모 프로세스가 `wait()` 없이 종료 → 자식(gpu_worker)은 PPID=1 고아 -- bash는 좀비 수거 안 함 → 좀비 잔존 -- worker를 `kill -9` → nvidia-smi에서 프로세스 사라져도 GPU 메모리 잔존 (orphaned context) -- Pod 삭제 후에도 GPU 메모리가 잔존할 수 있음 → 노드 재부팅 필요 가능 +검증 결과: +- ✅ 고아 발생 — `parent.py` 종료 후 `gpu_worker.py` PPID=1 reparent +- ✅ 좀비 발생 — kill -9 후 PID 1(sleep)이 reaping 안 함 +- ❌ **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** -> tini가 PID 1이면 좀비는 수거되지만, 코드에 signal handler가 없으면 CUDA cleanup은 여전히 실패한다. +> Tip: PID 1 = `sleep infinity` (bash exec 최적화로 sleep이 PID 1을 대체하지만, init 아닌 동작은 동일) -- tini의 `waitpid(-1)` 덕분에 좀비는 사라짐 ✅ -- 그러나 `kill -TERM`을 worker가 무시하므로 kubectl delete 시 gracePeriod 후 SIGKILL → orphaned context 여전히 발생 +**시나리오 2 — tini only** (미실험, 가설 갱신) +> 가설: 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`) -- prestop.sh가 GPU 점유 PID에 SIGTERM 전송 → 20초 대기 → **worker 무반응** → SIGKILL fallback -- 결과적으로 시나리오 2와 거의 동일 (단지 timing만 제어됨) +갱신된 가치 (시나리오 1 결과 반영): +- **좀비 reaping** — 시나리오 1의 좀비 발생을 막는 init 보호 +- ❌ **orphan CUDA ctx 방지 가치는 사실상 없음** (시나리오 1에서 driver가 자동 회수) +- ⚠️ **30초 grace 낭비는 그대로** (signal handler 없으므로 동일) -**시나리오 4 — Full Fix** -> tini + preStop + 코드 signal handler + 충분한 gracePeriod가 모두 갖춰지면 좀비·고아·orphaned context 모두 없다. +**시나리오 3 — tini + preStop** (미실험, 가설 갱신) +> 가설: 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 없음 -### 최종 비교표 (예상 결과) +### 최종 비교표 (S1 실측 + S2~4 갱신 예상) -| 항목 | S1 | S2 | S3 | S4 | +| 항목 | S1 (실측) | S2 (예상) | S3 (예상) | S4 (예상) | |---|---|---|---|---| -| 좀비 수거 | ❌ | ✅ | ✅ | ✅ | -| 고아 GPU 점유 | ❌ 잔존 | ❌ 잔존 | ⚠️ SIGKILL fallback | ✅ graceful | -| SIGTERM → CUDA cleanup | ❌ | ❌ (handler 없음) | ❌ (handler 없음) | ✅ | -| kill -9 후 orphaned ctx | ❌ 발생 | ❌ 발생 | ❌ 발생 가능 | N/A | -| Pod 삭제 후 GPU 메모리 | ❌ 잔존 가능 | ⚠️ 불확실 | ⚠️ 불확실 | ✅ 해제 | -| 노드 재부팅 필요 | 가능 | 가능 | 가능 | 불필요 | +| 좀비 수거 | ❌ 발생 (1~10개) | ✅ tini가 reap | ✅ | ✅ | +| 고아 발생 | ✅ (PPID=1) | ✅ but tini가 reap | ✅ but reap | 미발생 (handler가 정리) | +| SIGTERM 처리 | ❌ SIG_IGN | ❌ SIG_IGN | ❌ but preStop이 cleanup 시도 | ✅ graceful | +| kill -9 후 orphaned CUDA ctx | **❌ 미발생** (driver 회수) | 동일 예상 | 동일 예상 | N/A | +| 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`) 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) 노드 레벨에서 GPU 전체 상태 + ghost context 감지. diff --git a/plan.md b/plan.md index be7c287..997a61e 100644 --- a/plan.md +++ b/plan.md @@ -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 -- 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 -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 ``` -### 체크리스트 — 시나리오 1 +### 체크리스트 — 시나리오 1 (실측 결과 포함) -**재현 확인:** -- [ ] 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 매핑 확인 +> ⭐ 2026-04-13~14 실측 완료. ✅ 검증됨, ❌ plan.md 가설 틀림, ⭐ 새 발견. 자세히는 `results/scenario-1/notes.md`. -**kill -9 후 orphaned context 재현:** -- [ ] `kill -9 ` 실행 -- [ ] `ls /proc/` → "No such file or directory" 확인 -- [ ] `nvidia-smi` → GPU 메모리 여전히 점유 확인 → **orphaned context 재현** -- [ ] `fuser -v /dev/nvidia0` → 해당 PID 상태 확인 -- [ ] gpu-pod-trace.sh 재실행 → PID DEAD 또는 GHOST 상태 확인 +**재현 확인 (Step 1+2):** +- [x] PID 1 확인 — **`sleep infinity`** (bash exec 최적화로 sleep이 PID 1 대체. init 아닌 동작은 동일) +- [x] parent.py 실행 후 5초 뒤 부모 종료 확인 ✅ +- [x] gpu_worker.py가 고아가 됨 확인 (PPID=1) ✅ +- [x] 좀비는 kill -9 후 발생 (parent.py가 즉시 죽으면 좀비 단계 없이 사라짐) +- [x] GPU 메모리 점유 확인 (~2466 MiB) ✅ +- [x] gpu-pod-trace.sh로 PID → Pod 매핑 확인 ✅ +- [⚠️] observe.sh가 "ORPHANED" 오탐지 — torch가 노드 PID 반환 → 컨테이너에서 `/proc/<노드PID>` 미발견. **노드 트레이스로 정확 판정 가능.** -**Pod 삭제 테스트:** -- [ ] 터미널 1 (gpu-4): `watch -n 1 'nvidia-smi'` -- [ ] 터미널 2: `kubectl delete pod gpu-zombie-nofix -n gpu-zombie-test` -- [ ] Pod 삭제 후 GPU 메모리 해제 여부 기록 -- [ ] 잔존 시 gpu-pod-trace.sh로 ghost context 확인 -- [ ] 결과 저장 (`results/scenario-1/`) +**kill -9 후 orphaned context 재현 (Step 3) — ❌ 가설 틀림:** +- [x] `kill -9 ` 실행 +- [x] `ls /proc/` → "No such file" ✅ +- [❌] **`nvidia-smi` → GPU 메모리 0 MiB (즉시 해제)** ⭐ orphaned context **미발생** +- [x] `fuser -v /dev/nvidia0` → fd 잡는 프로세스 없음 ✅ +- [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 확인 -- [ ] 잔존 시 `nvidia-smi --gpu-reset` 시도 -- [ ] 안 되면 gpu-4 노드 재부팅 → GPU 정상 확인 +- [x] `nvidia-smi`로 GPU 메모리 0 확인 ✅ +- [❌] **`nvidia-smi --gpu-reset` 불필요** (가설 틀림) +- [❌] **노드 재부팅 불필요** (가설 틀림) + +### ⭐ 추가로 진행한 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) --- diff --git a/results/comparison.md b/results/comparison.md new file mode 100644 index 0000000..fa83ae6 --- /dev/null +++ b/results/comparison.md @@ -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 가설 자체를 "현대 환경에서 미발생"으로 정정