gpu-zombie-test/README.md

383 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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.

# 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-<scenario> -n gpu-zombie-test
# → ArgoCD selfHeal로 즉시 재배포
# 3. Pod 진입 & 실험
kubectl exec -it gpu-zombie-<scenario> -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 <worker_pid>로 orphaned context 유발
# - kubectl delete pod로 preStop 동작 확인
# 7. 결과 로그 저장 → results/scenario-<N>/
```
---
## 관찰 도구 — 무엇을 어떻게 확인하는가
### `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/<pid>/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-<scenario> -n gpu-zombie-test
# Pod 로그 (parent.py, gpu_worker.py 출력 + preStop 로그)
kubectl logs gpu-zombie-<scenario> -n gpu-zombie-test
# Pod 삭제 시 preStop 로그를 실시간으로 보려면
kubectl logs -f gpu-zombie-<scenario> -n gpu-zombie-test &
kubectl delete pod gpu-zombie-<scenario> -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 <worker>` 직후 `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 스택 사용 중