# GPU Zombie / Orphan Process & Orphaned CUDA Context 재현·해결 실험 Kubernetes 위에서 GPU 워크로드가 예기치 않게 종료되거나 signal handler가 잘못 짜였을 때 발생하는 **좀비 프로세스 / 고아 프로세스 / orphaned CUDA context / ghost GPU memory** 문제를 단계적으로 재현하고, 각 수정안(tini, preStop hook, code-level signal handler)이 어디까지 문제를 해결하는지 정량적으로 검증하는 실험 프로젝트. > 실험 설계 전문은 [`plan.md`](./plan.md) 참조. 이 README는 프로젝트 전체 개요와 사용법. --- ## 배경 — 왜 이 실험이 필요한가 GPU Pod 하나가 죽어도 GPU 메모리가 해제되지 않거나, `nvidia-smi`에는 프로세스가 안 보이는데 GPU 메모리는 잡혀 있는 현상(**ghost context**)은 현업에서 자주 관찰된다. 원인은 대체로: 1. **PID 1 문제** — 컨테이너의 PID 1이 `bash` 같은 non-init 프로세스면 `waitpid(-1)`을 호출하지 않아 좀비가 쌓임 2. **고아 프로세스** — 부모가 `wait()` 없이 종료하면 자식은 PID 1로 reparent → PID 1이 bash면 계속 살아남음 3. **Signal 전달 실패** — `kubectl delete pod` → SIGTERM → PID 1이 자식에게 전달 안 함 → gracePeriod 경과 후 SIGKILL 4. **Orphaned CUDA context** — 프로세스가 SIGKILL로 급사하면 CUDA driver가 미처 context를 정리하지 못해 GPU 메모리 잔존 5. **Ghost context** — `nvidia-smi --query-compute-apps`에는 안 잡히는데 `/dev/nvidia*`에 fd가 걸려 있거나 메모리만 점유된 상태 이 실험은 **점진적 수정안의 효과 범위**를 시나리오별로 분리해 측정하는 게 목표다. --- ## 실험 환경 | 항목 | 값 | |---|---| | 노드 | `gpu-4` (A100 SXM4 80GB × 4) | | 컨테이너 런타임 | containerd | | cgroup | **v1** (systemd slice 형식) | | K8s namespace | `gpu-zombie-test` | | 컨테이너 레지스트리 | `harbor.cone-chain.net/aipf` | | CI/CD | Gitea Actions + Harbor + ArgoCD | | 관리 권한 | `gpu-4` 노드 SSH + sudo 가능 | --- ## 시나리오 개요 총 **4개 시나리오**, **3개 이미지**, **4개 Pod**. (시나리오 2·3은 동일 이미지 `:tini`를 공유하며 Pod manifest의 preStop hook 유무로만 차이) | # | 이름 | PID 1 | preStop | Worker 코드 | 이미지 태그 | Pod 이름 | |---|---|---|---|---|---|---| | 1 | No Fix | `bash` | ❌ | SIGTERM 무시 | `:nofix` | `gpu-zombie-nofix` | | 2 | tini only | `tini` | ❌ | SIGTERM 무시 | `:tini` | `gpu-zombie-tini` | | 3 | tini + preStop | `tini` | ✅ | SIGTERM 무시 | `:tini` (재사용) | `gpu-zombie-tini-prestop` | | 4 | Full Fix | `tini` | ✅ | **SIGTERM handler + CUDA cleanup** | `:fullfix` | `gpu-zombie-fullfix` | ### 각 시나리오가 검증하는 가설 **시나리오 1 — No Fix** > PID 1이 bash면 좀비/고아/orphaned CUDA context가 모두 발생한다. - 부모 프로세스가 `wait()` 없이 종료 → 자식(gpu_worker)은 PPID=1 고아 - bash는 좀비 수거 안 함 → 좀비 잔존 - worker를 `kill -9` → nvidia-smi에서 프로세스 사라져도 GPU 메모리 잔존 (orphaned context) - Pod 삭제 후에도 GPU 메모리가 잔존할 수 있음 → 노드 재부팅 필요 가능 **시나리오 2 — tini only** > tini가 PID 1이면 좀비는 수거되지만, 코드에 signal handler가 없으면 CUDA cleanup은 여전히 실패한다. - tini의 `waitpid(-1)` 덕분에 좀비는 사라짐 ✅ - 그러나 `kill -TERM`을 worker가 무시하므로 kubectl delete 시 gracePeriod 후 SIGKILL → orphaned context 여전히 발생 **시나리오 3 — tini + preStop** > preStop hook이 SIGTERM을 보내도, 코드가 SIGTERM을 무시하면 결국 preStop timeout → SIGKILL fallback 경로를 타고 orphaned context는 여전히 발생한다. - Pod 삭제 → preStop hook 실행 (`/usr/local/bin/prestop.sh`) - prestop.sh가 GPU 점유 PID에 SIGTERM 전송 → 20초 대기 → **worker 무반응** → SIGKILL fallback - 결과적으로 시나리오 2와 거의 동일 (단지 timing만 제어됨) **시나리오 4 — Full Fix** > tini + preStop + 코드 signal handler + 충분한 gracePeriod가 모두 갖춰지면 좀비·고아·orphaned context 모두 없다. - worker가 SIGTERM handler에서 `tensors.clear()` → `torch.cuda.empty_cache()` → `torch.cuda.synchronize()` → exit - preStop은 동일한 prestop.sh를 호출하지만 worker가 graceful하게 응답하므로 SIGKILL fallback 경로 미트리거 - Pod 삭제 후 GPU 메모리 완전 해제, ghost context 없음 ### 최종 비교표 (예상 결과) | 항목 | S1 | S2 | S3 | S4 | |---|---|---|---|---| | 좀비 수거 | ❌ | ✅ | ✅ | ✅ | | 고아 GPU 점유 | ❌ 잔존 | ❌ 잔존 | ⚠️ SIGKILL fallback | ✅ graceful | | SIGTERM → CUDA cleanup | ❌ | ❌ (handler 없음) | ❌ (handler 없음) | ✅ | | kill -9 후 orphaned ctx | ❌ 발생 | ❌ 발생 | ❌ 발생 가능 | N/A | | Pod 삭제 후 GPU 메모리 | ❌ 잔존 가능 | ⚠️ 불확실 | ⚠️ 불확실 | ✅ 해제 | | 노드 재부팅 필요 | 가능 | 가능 | 가능 | 불필요 | --- ## 디렉토리 구조 ``` process_error/ ├── README.md # 이 문서 ├── plan.md # 실험 설계 전문 (1500+ 줄) ├── .claude/ │ └── settings.json # Claude Code 권한 설정 (이 프로젝트 한정) │ ├── .gitea/ │ └── workflows/ │ ├── ci.yaml # 시나리오 이미지 빌드/푸시 │ └── cd.yaml # Helm chart 패키징/푸시 │ ├── scripts/ # 공통 관찰·운영 스크립트 │ ├── observe.sh # 컨테이너 내부 관찰 (좀비/고아/GPU 상태) │ ├── node-observe.sh # 노드 레벨 GPU 관찰 │ ├── gpu-pod-trace.sh # GPU PID → Container → Pod 역추적 (노드 root 실행) │ └── prestop.sh # 시나리오 3·4 preStop hook 공용 스크립트 │ ├── scenario-1-nofix/ # 시나리오 1 — bash PID 1 │ ├── Dockerfile │ ├── parent.py # 자식 spawn 후 wait() 없이 종료 │ ├── gpu_worker.py # SIGTERM 무시, GPU ~2GB 점유 │ └── pod.yaml # manual apply용 (CI/CD 미사용 시) │ ├── scenario-2-tini-only/ # 시나리오 2 — tini PID 1 │ ├── Dockerfile │ ├── parent.py │ ├── gpu_worker.py # 시나리오 1과 동일 │ └── pod.yaml │ ├── scenario-3-tini-prestop/ # 시나리오 3 — tini + preStop │ └── pod.yaml # 이미지는 scenario-2 재사용 │ ├── scenario-4-fullfix/ # 시나리오 4 — 전부 적용 │ ├── Dockerfile │ ├── parent.py │ ├── gpu_worker_fixed.py # SIGTERM handler + CUDA cleanup │ └── pod.yaml │ ├── charts/ │ └── gpu-zombie-test/ # Helm chart (4개 pod template) │ ├── Chart.yaml │ ├── values.yaml # scenario enable 토글 │ └── templates/ │ ├── _helpers.tpl │ ├── pod-nofix.yaml │ ├── pod-tini.yaml │ ├── pod-tini-prestop.yaml │ └── pod-fullfix.yaml │ ├── argocd-app.yaml # ArgoCD Application 정의 │ └── results/ # 시나리오별 실험 결과 ├── scenario-1/ │ ├── observe-after-spawn.log │ ├── observe-after-kill.log │ ├── gpu-pod-trace.log │ ├── nvidia-smi-before-delete.log │ ├── nvidia-smi-after-delete.log │ └── notes.md ├── scenario-2/ ├── scenario-3/ ├── scenario-4/ └── comparison.md # 최종 비교 분석 ``` > **현재 상태:** `plan.md`, `README.md`, `.claude/settings.json`만 존재. > 나머지는 `plan.md`의 설계대로 Phase 0부터 순차 생성 예정. --- ## 실험 흐름 — 어떻게 동작하는가 ### Phase 0 — CI/CD 환경 구성 1. **Gitea repo 생성** (`soo-rnd/gpu-zombie-test`) + Secrets 등록 (REGISTRY_URL/USER/PASSWORD) 2. **K8s namespace + pull secret** 사전 생성 3. **`.gitea/workflows/ci.yaml`** 작성 — `scenario-*/` 변경 시 matrix로 3개 이미지를 Harbor에 빌드·푸시 4. **`.gitea/workflows/cd.yaml`** 작성 — `v*` tag push 시 Helm chart를 Harbor OCI에 푸시 5. **`charts/gpu-zombie-test/`** Helm chart 작성 — 4개 Pod template 6. **`argocd-app.yaml`** apply — 이후 chart 버전 업 시 ArgoCD가 자동 sync ``` [코드 변경] ├─ scenario-*/ 변경 → CI 트리거 │ └─ 3개 이미지 build → Harbor push (:nofix, :tini, :fullfix + sha 태그) │ └─ git tag v0.1.x → CD 트리거 └─ Helm chart package → Harbor OCI push └─ ArgoCD 자동 sync └─ gpu-zombie-test ns에 4개 Pod 배포 ``` ### Phase 1~2-C — 시나리오 실행 사이클 각 시나리오는 다음 공통 흐름을 따름: ```bash # 1. 코드 수정 → push → CI 자동 빌드 확인 (Gitea Actions UI) # 2. Pod 재생성으로 새 이미지 pull (imagePullPolicy: Always) kubectl delete pod gpu-zombie- -n gpu-zombie-test # → ArgoCD selfHeal로 즉시 재배포 # 3. Pod 진입 & 실험 kubectl exec -it gpu-zombie- -n gpu-zombie-test -- bash # 4. 컨테이너 내부 관찰 observe.sh # 좀비/고아/GPU 상태 # 5. 노드에서 역추적 (별도 터미널) ssh gpu-4 bash /path/to/gpu-pod-trace.sh # 6. 특정 액션 수행 후 재관찰 # - parent.py 실행 후 고아 생성 # - kill -9 로 orphaned context 유발 # - kubectl delete pod로 preStop 동작 확인 # 7. 결과 로그 저장 → results/scenario-/ ``` --- ## 관찰 도구 — 무엇을 어떻게 확인하는가 ### `scripts/observe.sh` (컨테이너 내부) 컨테이너 안에서 실행. 다음 8가지를 한 번에 점검: 1. PID 1 cmdline (bash vs tini) 2. 전체 프로세스 트리 (`ps auxf`) 3. 좀비 프로세스 (`STAT=Z`) 4. 고아 프로세스 (PPID=1) 5. GPU 메모리 상태 (`nvidia-smi --query-gpu`) 6. GPU 사용 프로세스 (`nvidia-smi --query-compute-apps`) 7. `/dev/nvidia*` fd 보유 프로세스 (`fuser`) 8. Orphaned GPU context 종합 체크 (nvidia-smi ∩ fuser ∩ /proc 생존 확인) ### `scripts/node-observe.sh` (gpu-4 노드 root) 노드 레벨에서 GPU 전체 상태 + ghost context 감지. ### `scripts/gpu-pod-trace.sh` (gpu-4 노드 root) ⭐ 핵심 cgroup v1 + containerd + systemd slice 포맷에 맞춰 작성된 **GPU PID → Pod 역추적** 스크립트. - `/proc//cgroup` 파싱 → container ID 추출 → `crictl inspect` → Pod name/namespace/container name - nvidia-smi에 안 잡히는 숨은 프로세스(`fuser`로만 감지)도 추적 - Ghost context (프로세스 0개인데 GPU 메모리 점유) 별도 경고 출력 예시: ``` PID STATE GPU_MEM NSMI QOS POD NAMESPACE CONTAINER 12345 R 2048 MiB Y besteffort gpu-zombie-nofix gpu-zombie-test worker ``` ### `scripts/prestop.sh` (Pod preStop hook) 시나리오 3·4의 이미지에 `/usr/local/bin/prestop.sh`로 포함. - GPU 점유 PID 수집 (nvidia-smi + fuser) - SIGTERM 전송 → 최대 20초 대기 - 미종료 시 SIGKILL fallback (시나리오 3에서 트리거) --- ## 결과는 어디서 확인하는가 ### 1. CI/CD 동작 확인 | 항목 | 위치 | |---|---| | CI 빌드 로그 | `https://gitea.inje-private.com/soo-rnd/gpu-zombie-test/actions` | | 푸시된 이미지 | `https://harbor.cone-chain.net/harbor/projects/aipf/repositories/gpu-zombie-test` | | 푸시된 Helm chart | Harbor 동 project OCI artifacts | | ArgoCD sync 상태 | `https://argocd.gpulive.cloud` → Application `gpu-zombie-test` | ### 2. Pod & 클러스터 상태 ```bash # 배포된 Pod 확인 kubectl get pods -n gpu-zombie-test # 특정 Pod 상세 (events, restarts, container status) kubectl describe pod gpu-zombie- -n gpu-zombie-test # Pod 로그 (parent.py, gpu_worker.py 출력 + preStop 로그) kubectl logs gpu-zombie- -n gpu-zombie-test # Pod 삭제 시 preStop 로그를 실시간으로 보려면 kubectl logs -f gpu-zombie- -n gpu-zombie-test & kubectl delete pod gpu-zombie- -n gpu-zombie-test ``` ### 3. GPU 상태 (노드에서) ```bash ssh ubuntu@gpu-4 sudo -i # 한 눈에 보기 watch -n 1 'nvidia-smi' # 전체 진단 bash /path/to/scripts/node-observe.sh # PID → Pod 역추적 bash /path/to/scripts/gpu-pod-trace.sh ``` ### 4. 실험 결과 파일 (`results/`) 각 시나리오별로 다음 파일들을 저장: | 파일 | 내용 | |---|---| | `observe-after-spawn.log` | parent.py 실행 직후 `observe.sh` 출력 | | `observe-after-kill.log` | `kill -9 ` 직후 `observe.sh` 출력 — orphaned context 확인 | | `gpu-pod-trace.log` | 노드에서 `gpu-pod-trace.sh` 출력 — PID→Pod 매핑 | | `nvidia-smi-before-delete.log` | Pod 삭제 직전 GPU 상태 | | `nvidia-smi-after-delete.log` | Pod 삭제 직후 GPU 상태 (메모리 잔존 여부) | | `notes.md` | 해당 시나리오 특이사항, 예상과 다른 결과, 타이밍 이슈 등 | ### 5. 최종 비교 — `results/comparison.md` 모든 시나리오 완료 후, 4개 결과를 한 표로 정리하고 각 수정안의 효과를 정량화. plan.md의 "Phase 3: 결과 비교" 템플릿 사용. --- ## Quick Start **전제:** `plan.md` 기준 Phase 0 (CI/CD 환경) 완료 상태. ```bash # 1. 클러스터 접근 확인 kubectl config current-context kubectl get ns gpu-zombie-test # 2. 배포된 Pod 확인 (ArgoCD가 sync 완료한 상태) kubectl get pods -n gpu-zombie-test # NAME READY STATUS ... # gpu-zombie-nofix 1/1 Running # gpu-zombie-tini 1/1 Running # gpu-zombie-tini-prestop 1/1 Running # gpu-zombie-fullfix 1/1 Running # 3. 시나리오 1부터 순차 실험 — plan.md Phase 1 참조 kubectl exec -it gpu-zombie-nofix -n gpu-zombie-test -- bash # (컨테이너 내부) python3 parent.py & sleep 10 observe.sh # 4. 노드에서 역추적 ssh ubuntu@gpu-4 sudo bash gpu-pod-trace.sh # 5. 결과 저장 (로컬) mkdir -p results/scenario-1 kubectl exec gpu-zombie-nofix -n gpu-zombie-test -- observe.sh > results/scenario-1/observe-after-spawn.log # 6. 다음 시나리오 전 GPU 초기화 확인 nvidia-smi # (필요 시 nvidia-smi --gpu-reset 또는 노드 재부팅) ``` --- ## 주의사항 1. **실험은 `gpu-4` 노드를 독점한다** — 다른 GPU 워크로드가 있으면 간섭 가능. 실험 전 `nvidia-smi`로 깨끗한 상태 확인. 2. **시나리오 간 GPU 초기화 필수** — orphaned/ghost context가 누적되면 후속 시나리오 결과가 오염된다. 정리 안 되면 `nvidia-smi --gpu-reset` 시도, 안 되면 **gpu-4 노드 재부팅**. 3. **`nvidia-smi --gpu-reset`은 해당 GPU를 쓰는 다른 컨텍스트가 있으면 실패** — 모든 GPU pod 정리 후 시도. 4. **`prestop.sh`는 이미지 2개(`:tini`, `:fullfix`)에만 포함** — 시나리오 1의 `:nofix` 이미지에는 없음. 5. **시나리오 2와 3은 같은 이미지를 쓴다** — scenario-3는 별도 이미지를 빌드하지 않고 `:tini` 재사용. CI matrix에 scenario-3가 없는 것은 의도된 것. --- ## 참고 문서 - **설계 전문:** [`plan.md`](./plan.md) — 모든 스크립트·매니페스트·체크리스트 포함 (1500+ 줄) - **Claude Code 권한:** [`.claude/settings.json`](./.claude/settings.json) — 이 프로젝트 한정 권한 - **CI/CD 참고 템플릿:** `/home/ubuntu/soo/vLLM/A100_npu/` — 동일 Gitea/Harbor/ArgoCD 스택 사용 중