From 1daa9bad249fe02313313e2d07a5600518237ec8 Mon Sep 17 00:00:00 2001 From: selee Date: Fri, 10 Apr 2026 17:12:38 +0900 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20GPU=20zombie/orp?= =?UTF-8?q?han/orphaned-CUDA-context=20=EC=8B=A4=ED=97=98=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plan.md: Phase 0 (CI/CD) + 4 시나리오 (No Fix / tini only / tini+preStop / Full Fix) - scripts/: observe, node-observe, gpu-pod-trace (cgroup v1), prestop - scenario-1~4/: Dockerfile + worker py + Pod manifest - .gitea/workflows/: ci.yaml (matrix 3 이미지 build), cd.yaml (v* tag → Helm OCI) - charts/gpu-zombie-test/: Helm chart (4 Pod template + helper) - argocd-app.yaml: Harbor OCI chart auto sync Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitea/workflows/cd.yaml | 41 + .gitea/workflows/ci.yaml | 59 + .gitignore | 6 + README.md | 382 +++++ argocd-app.yaml | 22 + charts/gpu-zombie-test/Chart.yaml | 6 + charts/gpu-zombie-test/templates/_helpers.tpl | 17 + .../templates/pod-fullfix.yaml | 27 + .../gpu-zombie-test/templates/pod-nofix.yaml | 11 + .../templates/pod-tini-prestop.yaml | 27 + .../gpu-zombie-test/templates/pod-tini.yaml | 11 + charts/gpu-zombie-test/values.yaml | 25 + plan.md | 1517 +++++++++++++++++ results/.gitkeep | 0 scenario-1-nofix/Dockerfile | 14 + scenario-1-nofix/gpu_worker.py | 24 + scenario-1-nofix/parent.py | 14 + scenario-1-nofix/pod.yaml | 21 + scenario-2-tini-only/Dockerfile | 18 + scenario-2-tini-only/gpu_worker.py | 22 + scenario-2-tini-only/parent.py | 12 + scenario-2-tini-only/pod.yaml | 21 + scenario-3-tini-prestop/pod.yaml | 29 + scenario-4-fullfix/Dockerfile | 14 + scenario-4-fullfix/gpu_worker_fixed.py | 44 + scenario-4-fullfix/parent.py | 12 + scenario-4-fullfix/pod.yaml | 27 + scripts/gpu-pod-trace.sh | 188 ++ scripts/node-observe.sh | 41 + scripts/observe.sh | 110 ++ scripts/prestop.sh | 49 + 31 files changed, 2811 insertions(+) create mode 100644 .gitea/workflows/cd.yaml create mode 100644 .gitea/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 argocd-app.yaml create mode 100644 charts/gpu-zombie-test/Chart.yaml create mode 100644 charts/gpu-zombie-test/templates/_helpers.tpl create mode 100644 charts/gpu-zombie-test/templates/pod-fullfix.yaml create mode 100644 charts/gpu-zombie-test/templates/pod-nofix.yaml create mode 100644 charts/gpu-zombie-test/templates/pod-tini-prestop.yaml create mode 100644 charts/gpu-zombie-test/templates/pod-tini.yaml create mode 100644 charts/gpu-zombie-test/values.yaml create mode 100644 plan.md create mode 100644 results/.gitkeep create mode 100644 scenario-1-nofix/Dockerfile create mode 100644 scenario-1-nofix/gpu_worker.py create mode 100644 scenario-1-nofix/parent.py create mode 100644 scenario-1-nofix/pod.yaml create mode 100644 scenario-2-tini-only/Dockerfile create mode 100644 scenario-2-tini-only/gpu_worker.py create mode 100644 scenario-2-tini-only/parent.py create mode 100644 scenario-2-tini-only/pod.yaml create mode 100644 scenario-3-tini-prestop/pod.yaml create mode 100644 scenario-4-fullfix/Dockerfile create mode 100644 scenario-4-fullfix/gpu_worker_fixed.py create mode 100644 scenario-4-fullfix/parent.py create mode 100644 scenario-4-fullfix/pod.yaml create mode 100755 scripts/gpu-pod-trace.sh create mode 100755 scripts/node-observe.sh create mode 100755 scripts/observe.sh create mode 100755 scripts/prestop.sh diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml new file mode 100644 index 0000000..93e240c --- /dev/null +++ b/.gitea/workflows/cd.yaml @@ -0,0 +1,41 @@ +name: CD — Package & Deploy Helm Chart + +on: + push: + tags: ['v*'] + +env: + CHART_NAME: gpu-zombie-test + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set versions + run: | + TAG=${GITHUB_REF#refs/tags/} + echo "TAG=${TAG}" >> $GITHUB_ENV + echo "CHART_VERSION=${TAG#v}" >> $GITHUB_ENV + + - name: Install Helm + run: | + curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + + - name: Package Helm chart + run: | + sed -i "s/appVersion: .*/appVersion: \"${{ env.TAG }}\"/" \ + charts/${CHART_NAME}/Chart.yaml + sed -i "s/^version: .*/version: ${{ env.CHART_VERSION }}/" \ + charts/${CHART_NAME}/Chart.yaml + helm package charts/${CHART_NAME} + + - name: Push Helm chart to Harbor (OCI) + run: | + echo '${{ secrets.REGISTRY_PASSWORD }}' | \ + helm registry login ${{ secrets.REGISTRY_URL }} \ + -u ${{ secrets.REGISTRY_USER }} --password-stdin + helm push ${CHART_NAME}-${{ env.CHART_VERSION }}.tgz \ + oci://${{ secrets.REGISTRY_URL }}/aipf diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..66b93e2 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,59 @@ +name: CI — Build & Push Scenario Images + +on: + push: + branches: [main] + paths: + - 'scenario-*/Dockerfile' + - 'scenario-*/parent.py' + - 'scenario-*/gpu_worker*.py' + - 'scripts/observe.sh' + - 'scripts/prestop.sh' + - '.gitea/workflows/ci.yaml' + +jobs: + build-and-push: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - scenario: scenario-1-nofix + tag: nofix + - scenario: scenario-2-tini-only + tag: tini + - scenario: scenario-4-fullfix + tag: fullfix + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set image tag + run: | + SHORT_SHA=$(echo ${GITHUB_SHA} | cut -c1-7) + echo "SHA_TAG=${{ matrix.tag }}-sha-${SHORT_SHA}" >> $GITHUB_ENV + + - name: Login to Harbor + run: | + for i in 1 2 3; do + echo '${{ secrets.REGISTRY_PASSWORD }}' | \ + docker login ${{ secrets.REGISTRY_URL }} \ + -u ${{ secrets.REGISTRY_USER }} --password-stdin && break + echo "Retry $i..."; sleep 5 + done + + - name: Prepare build context (observe.sh + prestop.sh 포함) + run: | + cp scripts/observe.sh ${{ matrix.scenario }}/observe.sh + # prestop.sh는 tini/fullfix 이미지에만 필요하지만, + # 모든 컨텍스트에 복사해두고 Dockerfile COPY 여부로 제어 (nofix는 미사용) + cp scripts/prestop.sh ${{ matrix.scenario }}/prestop.sh + + - name: Build & push image + run: | + IMAGE=${{ secrets.REGISTRY_URL }}/aipf/gpu-zombie-test + docker build \ + -t ${IMAGE}:${{ env.SHA_TAG }} \ + -t ${IMAGE}:${{ matrix.tag }} \ + ${{ matrix.scenario }} + docker push ${IMAGE}:${{ env.SHA_TAG }} + docker push ${IMAGE}:${{ matrix.tag }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8d97eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.pyo +results/*/ +!results/.gitkeep +.claude/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3747ad --- /dev/null +++ b/README.md @@ -0,0 +1,382 @@ +# 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 스택 사용 중 diff --git a/argocd-app.yaml b/argocd-app.yaml new file mode 100644 index 0000000..22087ce --- /dev/null +++ b/argocd-app.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gpu-zombie-test + namespace: argocd +spec: + project: default + source: + chart: gpu-zombie-test + repoURL: harbor.cone-chain.net/aipf + targetRevision: ">0.0.0" + helm: + releaseName: gpu-zombie-test + destination: + server: https://kubernetes.default.svc + namespace: gpu-zombie-test + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/charts/gpu-zombie-test/Chart.yaml b/charts/gpu-zombie-test/Chart.yaml new file mode 100644 index 0000000..1a4f27e --- /dev/null +++ b/charts/gpu-zombie-test/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: gpu-zombie-test +description: GPU zombie/orphan/orphaned-CUDA-context reproduction experiment +type: application +version: 0.1.0 +appVersion: "v0.1.0" diff --git a/charts/gpu-zombie-test/templates/_helpers.tpl b/charts/gpu-zombie-test/templates/_helpers.tpl new file mode 100644 index 0000000..9558c7d --- /dev/null +++ b/charts/gpu-zombie-test/templates/_helpers.tpl @@ -0,0 +1,17 @@ +{{/* +공용 Pod spec (preStop 없는 시나리오용) +*/}} +{{- define "gpu-zombie.podSpec" -}} +nodeName: {{ .root.Values.node }} +terminationGracePeriodSeconds: {{ .scenario.gracePeriod }} +restartPolicy: Never +imagePullSecrets: + - name: {{ .root.Values.image.pullSecret }} +containers: + - name: worker + image: "{{ .root.Values.image.repository }}:{{ .scenario.tag }}" + imagePullPolicy: {{ .root.Values.image.pullPolicy }} + resources: + limits: + nvidia.com/gpu: 1 +{{- end }} diff --git a/charts/gpu-zombie-test/templates/pod-fullfix.yaml b/charts/gpu-zombie-test/templates/pod-fullfix.yaml new file mode 100644 index 0000000..080a7f7 --- /dev/null +++ b/charts/gpu-zombie-test/templates/pod-fullfix.yaml @@ -0,0 +1,27 @@ +{{- if .Values.scenarios.fullfix.enabled }} +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-fullfix + labels: + app: gpu-zombie-test + scenario: fullfix +spec: + nodeName: {{ .Values.node }} + terminationGracePeriodSeconds: {{ .Values.scenarios.fullfix.gracePeriod }} + restartPolicy: Never + imagePullSecrets: + - name: {{ .Values.image.pullSecret }} + containers: + - name: worker + image: "{{ .Values.image.repository }}:{{ .Values.scenarios.fullfix.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: + limits: + nvidia.com/gpu: 1 + lifecycle: + preStop: + exec: + # 공용 prestop.sh — 시나리오 3과 동일 스크립트 + command: ["/bin/sh", "-c", "/usr/local/bin/prestop.sh"] +{{- end }} diff --git a/charts/gpu-zombie-test/templates/pod-nofix.yaml b/charts/gpu-zombie-test/templates/pod-nofix.yaml new file mode 100644 index 0000000..6682f68 --- /dev/null +++ b/charts/gpu-zombie-test/templates/pod-nofix.yaml @@ -0,0 +1,11 @@ +{{- if .Values.scenarios.nofix.enabled }} +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-nofix + labels: + app: gpu-zombie-test + scenario: nofix +spec: + {{- include "gpu-zombie.podSpec" (dict "root" . "scenario" .Values.scenarios.nofix) | nindent 2 }} +{{- end }} diff --git a/charts/gpu-zombie-test/templates/pod-tini-prestop.yaml b/charts/gpu-zombie-test/templates/pod-tini-prestop.yaml new file mode 100644 index 0000000..303293e --- /dev/null +++ b/charts/gpu-zombie-test/templates/pod-tini-prestop.yaml @@ -0,0 +1,27 @@ +{{- if .Values.scenarios.tiniPrestop.enabled }} +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-tini-prestop + labels: + app: gpu-zombie-test + scenario: tini-prestop +spec: + nodeName: {{ .Values.node }} + terminationGracePeriodSeconds: {{ .Values.scenarios.tiniPrestop.gracePeriod }} + restartPolicy: Never + imagePullSecrets: + - name: {{ .Values.image.pullSecret }} + containers: + - name: worker + image: "{{ .Values.image.repository }}:{{ .Values.scenarios.tiniPrestop.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: + limits: + nvidia.com/gpu: 1 + lifecycle: + preStop: + exec: + # 공용 prestop.sh — 이미지에 포함됨 (scripts/prestop.sh 참조) + command: ["/bin/sh", "-c", "/usr/local/bin/prestop.sh"] +{{- end }} diff --git a/charts/gpu-zombie-test/templates/pod-tini.yaml b/charts/gpu-zombie-test/templates/pod-tini.yaml new file mode 100644 index 0000000..6c482b3 --- /dev/null +++ b/charts/gpu-zombie-test/templates/pod-tini.yaml @@ -0,0 +1,11 @@ +{{- if .Values.scenarios.tini.enabled }} +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-tini + labels: + app: gpu-zombie-test + scenario: tini +spec: + {{- include "gpu-zombie.podSpec" (dict "root" . "scenario" .Values.scenarios.tini) | nindent 2 }} +{{- end }} diff --git a/charts/gpu-zombie-test/values.yaml b/charts/gpu-zombie-test/values.yaml new file mode 100644 index 0000000..250828c --- /dev/null +++ b/charts/gpu-zombie-test/values.yaml @@ -0,0 +1,25 @@ +image: + repository: harbor.cone-chain.net/aipf/gpu-zombie-test + pullPolicy: Always + pullSecret: harbor-pull-secret + +node: gpu-4 + +# 시나리오별 토글 — 실험 격리가 필요하면 false 처리 후 chart 버전 bump +scenarios: + nofix: + enabled: true + tag: nofix + gracePeriod: 30 + tini: + enabled: true + tag: tini + gracePeriod: 30 + tiniPrestop: + enabled: true + tag: tini # scenario-2와 동일 이미지 + gracePeriod: 60 + fullfix: + enabled: true + tag: fullfix + gracePeriod: 90 diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..be7c287 --- /dev/null +++ b/plan.md @@ -0,0 +1,1517 @@ +# GPU Zombie/Orphan Process & Orphaned CUDA Context 재현 및 해결 실험 계획서 + +## 실험 환경 + +| 항목 | 값 | +|---|---| +| 노드 | `gpu-4` (A100) | +| 재부팅 가능 여부 | O | +| 컨테이너 런타임 | containerd | +| cgroup | **v1 (systemd slice 형식)** | +| 실험 도구 | Claude CLI (claude code) | + +### cgroup v1 경로 형식 (확인 완료) + +``` +# /proc//cgroup 출력 형태: +9:memory:/kubepods.slice/kubepods-.slice/kubepods--pod.slice/cri-containerd-.scope + +# 실제 확인된 예시: +9:memory:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4432d9c6_f213_41de_94eb_61ce8fa70f9b.slice/cri-containerd-327289ee8e99a9c53b46e737f01eb0e54373f3cdca986a60e88c21908e0c8a92.scope + +# 파싱 포인트: +# Container ID → cri-containerd- 뒤의 64자 hex (.scope 제거) +# Pod UID → pod 뒤의 언더스코어 구분 UUID (하이픈으로 변환 필요) +# QoS → kubepods-besteffort / kubepods-burstable / kubepods.slice 바로 아래면 guaranteed +``` + +--- + +## 사전 준비 + +### 체크리스트 + +- [ ] `gpu-4` 노드에 SSH 접속 가능 확인 +- [ ] `gpu-4` 노드에 GPU가 정상 동작하는지 확인 (`nvidia-smi`) +- [ ] `gpu-4` 노드에 다른 GPU 워크로드가 없는지 확인 (실험 간 간섭 방지) +- [ ] `kubectl` 클러스터 접근 권한 확인 +- [ ] 실험용 namespace 생성 (`kubectl create ns gpu-zombie-test`) +- [ ] 컨테이너 이미지 빌드 환경 준비 (docker 또는 podman) +- [ ] 컨테이너 레지스트리 push 가능 확인 +- [ ] `gpu-4` 노드에 `crictl` 설치 확인 (`crictl version`) +- [ ] `gpu-4` 노드에 `fuser` 설치 확인 (`which fuser`, 없으면 `apt install psmisc`) +- [ ] `gpu-4` 노드에 `jq` 설치 확인 (`jq --version`, 없으면 `apt install jq`) + +### 디렉토리 구조 + +``` +gpu-zombie-test/ +├── plan.md # 이 문서 +├── scripts/ +│ ├── observe.sh # 컨테이너 내부 관찰 스크립트 +│ ├── node-observe.sh # 노드 레벨 GPU 관찰 스크립트 +│ ├── gpu-pod-trace.sh # GPU PID → Pod 역추적 스크립트 (노드에서 실행) +│ └── prestop.sh # 시나리오 3·4 preStop hook 공용 스크립트 +├── scenario-1-nofix/ +│ ├── Dockerfile +│ ├── parent.py +│ ├── gpu_worker.py +│ └── pod.yaml +├── scenario-2-tini-only/ +│ ├── Dockerfile +│ ├── parent.py +│ ├── gpu_worker.py # SIGTERM 무시 (signal handler 없음) +│ └── pod.yaml +├── scenario-3-tini-prestop/ +│ ├── Dockerfile +│ ├── parent.py +│ ├── gpu_worker.py # SIGTERM 무시 (signal handler 없음) +│ └── pod.yaml +├── scenario-4-fullfix/ +│ ├── Dockerfile +│ ├── parent.py +│ ├── gpu_worker_fixed.py # SIGTERM handler + CUDA cleanup 포함 +│ └── pod.yaml +└── results/ + └── comparison.md # 최종 결과 비교표 +``` + +--- + +## 공통 스크립트 + +### scripts/observe.sh (컨테이너 내부에서 실행) + +```bash +#!/bin/bash +echo "==========================================" +echo "컨테이너 내부 관찰: $(date)" +echo "==========================================" + +echo "" +echo "[1] PID 1 확인" +cat /proc/1/cmdline | tr '\0' ' ' +echo "" + +echo "" +echo "[2] 전체 프로세스 트리" +ps auxf +echo "" + +echo "" +echo "[3] 좀비 프로세스 (STAT=Z)" +ZOMBIES=$(ps aux | awk '$8 ~ /Z/') +if [ -z "$ZOMBIES" ]; then + echo " → 좀비 없음 ✅" +else + echo " → 좀비 발견 ❌" + echo "$ZOMBIES" +fi +echo "" + +echo "" +echo "[4] 고아 프로세스 (PPID=1, PID 1 자체 제외)" +ORPHANS=$(ps -eo pid,ppid,stat,cmd | awk '$2==1 && $1!=1') +if [ -z "$ORPHANS" ]; then + echo " → 고아 없음 ✅" +else + echo " → 고아 발견 ⚠️" + echo "$ORPHANS" +fi +echo "" + +echo "" +echo "[5] GPU 메모리 상태" +nvidia-smi --query-gpu=index,memory.used,memory.total --format=csv +echo "" + +echo "" +echo "[6] GPU 사용 프로세스 (nvidia-smi)" +GPU_PIDS=$(nvidia-smi --query-compute-apps=pid,used_gpu_memory,name --format=csv,noheader 2>/dev/null) +if [ -z "$GPU_PIDS" ]; then + echo " → nvidia-smi에 프로세스 없음" +else + echo "$GPU_PIDS" +fi +echo "" + +echo "" +echo "[7] /dev/nvidia* fd 보유 프로세스 (nvidia-smi에 안 나올 수 있음)" +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm /dev/nvidiactl; do + [ -e "$dev" ] || continue + PIDS=$(fuser "$dev" 2>/dev/null) + if [ -n "$PIDS" ]; then + echo " $dev →$PIDS" + fi +done +echo "" + +echo "" +echo "[8] Orphaned GPU Context 종합 체크" + +# 8-a. nvidia-smi에 있는 PID 중 죽은 것 +echo " 8-a. nvidia-smi 등록 프로세스 생존 확인:" +NSMI_PIDS=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null) +if [ -z "$NSMI_PIDS" ]; then + echo " nvidia-smi에 등록된 프로세스 없음" +else + for pid in $NSMI_PIDS; do + if [ -d "/proc/$pid" ]; then + STATE=$(cat /proc/$pid/status 2>/dev/null | grep "^State:" | awk '{print $2}') + echo " PID $pid: 살아있음 (State=$STATE)" + else + echo " PID $pid: 죽었음 → ❌ ORPHANED GPU CONTEXT" + fi + done +fi + +# 8-b. nvidia-smi에 안 나오지만 fuser에 잡히는 프로세스 +echo " 8-b. nvidia-smi 미등록 but GPU fd 보유:" +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm /dev/nvidiactl; do + [ -e "$dev" ] || continue + for pid in $(fuser "$dev" 2>/dev/null); do + if ! echo "$NSMI_PIDS" | grep -q "^${pid}$"; then + if [ -d "/proc/$pid" ]; then + CMD=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | head -c 50) + echo " ⚠️ PID $pid ($dev): $CMD" + fi + fi + done +done + +# 8-c. Ghost context (프로세스 0인데 메모리 점유) +echo " 8-c. Ghost context 체크:" +PROC_COUNT=$(echo "$NSMI_PIDS" | grep -v '^$' | wc -l) +nvidia-smi --query-gpu=index,memory.used --format=csv,noheader,nounits 2>/dev/null | while IFS=', ' read -r idx mem; do + if [ "$PROC_COUNT" -eq 0 ] && [ "$mem" -gt 100 ]; then + echo " ⚠️ GPU $idx: 프로세스 없는데 ${mem}MiB 점유 → GHOST CONTEXT" + else + echo " GPU $idx: ${mem}MiB (프로세스 ${PROC_COUNT}개)" + fi +done + +echo "" +echo "==========================================" +``` + +### scripts/node-observe.sh (gpu-4 노드에서 root로 실행) + +```bash +#!/bin/bash +echo "==========================================" +echo "노드 레벨 GPU 관찰: $(date)" +echo "==========================================" +echo "" +echo "[1] nvidia-smi 전체" +nvidia-smi +echo "" +echo "[2] GPU 프로세스 목록" +nvidia-smi --query-compute-apps=pid,used_gpu_memory,name --format=csv +echo "" +echo "[3] /dev/nvidia* 사용 프로세스 (fuser)" +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm /dev/nvidiactl; do + [ -e "$dev" ] || continue + PIDS=$(fuser "$dev" 2>/dev/null) + if [ -n "$PIDS" ]; then + echo " $dev →$PIDS" + fi +done +echo "" +echo "[4] nvidia-smi 미등록 but /dev/nvidia fd 보유 프로세스" +NVIDIA_PIDS=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null | sort -u) +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm /dev/nvidiactl; do + [ -e "$dev" ] || continue + for pid in $(fuser "$dev" 2>/dev/null); do + if ! echo "$NVIDIA_PIDS" | grep -q "^${pid}$"; then + CMD=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | head -c 60) + echo " ⚠️ PID $pid ($dev): nvidia-smi 미등록, CMD: $CMD" + fi + done +done +echo "" +echo "[5] Ghost context 체크 (프로세스 0 but 메모리 점유)" +PROC_COUNT=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null | grep -v '^$' | wc -l) +nvidia-smi --query-gpu=index,memory.used --format=csv,noheader,nounits 2>/dev/null | while IFS=', ' read -r idx mem; do + if [ "$PROC_COUNT" -eq 0 ] && [ "$mem" -gt 100 ]; then + echo " ⚠️ GPU $idx: 프로세스 0개인데 ${mem}MiB 점유 → GHOST CONTEXT" + fi +done +echo "==========================================" +``` + +### scripts/prestop.sh (시나리오 3·4 컨테이너 이미지에 포함) + +시나리오 3·4의 preStop hook이 호출하는 단일 스크립트. +SIGTERM 전송 → 최대 20초 대기 → SIGKILL fallback의 슈퍼셋 동작. +- 시나리오 3 (`tini` 이미지, worker가 SIGTERM 무시): fallback 경로가 실제로 트리거됨 +- 시나리오 4 (`fullfix` 이미지, worker가 SIGTERM 처리): fallback 경로 미트리거 (graceful 종료) + +```bash +#!/bin/sh +# /usr/local/bin/prestop.sh +# Pod preStop hook용 GPU cleanup 스크립트 +# 시나리오 3·4 공통 사용 + +echo "[preStop] GPU cleanup 시작: $(date)" + +# 1. nvidia-smi + fuser로 GPU 점유 PID 수집 +GPU_PIDS=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null) +FUSER_PIDS="" +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm; do + [ -e "$dev" ] || continue + PIDS=$(fuser "$dev" 2>/dev/null) + FUSER_PIDS="$FUSER_PIDS $PIDS" +done +ALL_PIDS=$(echo "$GPU_PIDS $FUSER_PIDS" | tr ' ' '\n' | sort -u | grep -v '^$') +echo "[preStop] 전체 GPU 관련 PIDs: $ALL_PIDS" + +# 2. PID 1(tini) 제외하고 SIGTERM 전송 +for pid in $ALL_PIDS; do + if [ "$pid" != "1" ]; then + echo "[preStop] SIGTERM -> PID $pid" + kill -TERM $pid 2>/dev/null + fi +done + +# 3. 최대 20초 대기 — graceful cleanup 기다림 +for i in $(seq 1 20); do + REMAINING=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null | wc -l) + if [ "$REMAINING" -eq 0 ]; then + echo "[preStop] GPU 프로세스 정리 완료 ✅" + break + fi + echo "[preStop] 대기 ($i/20) 남은 프로세스: $REMAINING" + sleep 1 +done + +# 4. SIGKILL fallback (시나리오 3에서 트리거됨) +STILL=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null | wc -l) +if [ "$STILL" -gt 0 ]; then + echo "[preStop] ⚠️ SIGTERM 무시됨 → SIGKILL fallback" + for pid in $(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null); do + kill -9 $pid 2>/dev/null + done +fi + +echo "[preStop] 완료: $(date)" +``` + +### scripts/gpu-pod-trace.sh (gpu-4 노드에서 root로 실행) + +cgroup v1 + containerd + systemd slice 형식에 맞춰 작성. + +```bash +#!/bin/bash +# GPU Process → Container → Pod 역추적 스크립트 +# 환경: cgroup v1, containerd, systemd slice 형식 +# gpu-4 노드에서 root로 실행 + +echo "============================================" +echo "GPU Process → Pod 매핑 (cgroup v1)" +echo "시각: $(date)" +echo "노드: $(hostname)" +echo "============================================" + +# --- 함수 정의 --- + +extract_container_id() { + local pid=$1 + # cgroup v1 systemd slice: cri-containerd-<64hex>.scope + grep ':memory:' /proc/$pid/cgroup 2>/dev/null | grep -oP 'cri-containerd-\K[a-f0-9]{64}' +} + +extract_pod_uid() { + local pid=$1 + # kubepods--pod.slice → 하이픈 변환 + local raw=$(grep ':memory:' /proc/$pid/cgroup 2>/dev/null | grep -oP 'pod\K[a-f0-9_]+(?=\.slice)') + if [ -n "$raw" ]; then + echo "$raw" | tr '_' '-' + fi +} + +extract_qos() { + local pid=$1 + local qos=$(grep ':memory:' /proc/$pid/cgroup 2>/dev/null | grep -oP 'kubepods-\K(besteffort|burstable)(?=[\.-])' | head -1) + if [ -n "$qos" ]; then + echo "$qos" + else + grep ':memory:' /proc/$pid/cgroup 2>/dev/null | grep -q 'kubepods' && echo "guaranteed" || echo "host" + fi +} + +get_pod_info() { + local container_id=$1 + local info=$(crictl inspect "$container_id" 2>/dev/null) + [ -z "$info" ] && return + + local pod=$(echo "$info" | grep -oP '"io.kubernetes.pod.name":\s*"\K[^"]+' | head -1) + local ns=$(echo "$info" | grep -oP '"io.kubernetes.pod.namespace":\s*"\K[^"]+' | head -1) + local cont=$(echo "$info" | grep -oP '"io.kubernetes.container.name":\s*"\K[^"]+' | head -1) + echo "${pod}|${ns}|${cont}" +} + +# --- 1단계: GPU 점유 PID 수집 --- + +echo "" +echo "[1] GPU 점유 프로세스 수집 (nvidia-smi + fuser)" + +NVIDIA_PIDS=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null | sort -u) + +FUSER_PIDS="" +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm /dev/nvidiactl; do + [ -e "$dev" ] || continue + PIDS=$(fuser "$dev" 2>/dev/null) + FUSER_PIDS="$FUSER_PIDS $PIDS" +done +FUSER_PIDS=$(echo $FUSER_PIDS | tr ' ' '\n' | sort -u | grep -v '^$') + +ALL_PIDS=$(echo -e "${NVIDIA_PIDS}\n${FUSER_PIDS}" | sort -u | grep -v '^$') + +if [ -z "$ALL_PIDS" ]; then + echo " GPU 점유 프로세스 없음" + echo "" + echo "[!] Ghost Context 체크:" + nvidia-smi --query-gpu=index,memory.used --format=csv,noheader,nounits 2>/dev/null | while IFS=', ' read -r idx mem; do + if [ "$mem" -gt 100 ]; then + echo " ⚠️ GPU $idx: 프로세스 없는데 ${mem}MiB 점유 → GHOST CONTEXT" + echo " 추적 불가. nvidia-smi --gpu-reset -i $idx 또는 노드 재부팅 필요" + else + echo " ✅ GPU $idx: ${mem}MiB (정상)" + fi + done + exit 0 +fi + +echo " nvidia-smi PIDs: $(echo $NVIDIA_PIDS | tr '\n' ' ')" +echo " fuser PIDs: $(echo $FUSER_PIDS | tr '\n' ' ')" + +# --- 2단계: PID → Container → Pod 매핑 --- + +echo "" +echo "[2] PID → Container → Pod 매핑" +echo "--------------------------------------------------------------------------------------------------------------" +printf "%-8s %-6s %-12s %-8s %-12s %-30s %-20s %-15s\n" \ + "PID" "STATE" "GPU_MEM" "NSMI" "QOS" "POD" "NAMESPACE" "CONTAINER" +echo "--------------------------------------------------------------------------------------------------------------" + +for pid in $ALL_PIDS; do + + IN_NSMI="N" + echo "$NVIDIA_PIDS" | grep -q "^${pid}$" && IN_NSMI="Y" + + # 프로세스 생존 + if [ ! -d "/proc/$pid" ]; then + printf "%-8s %-6s %-12s %-8s %-12s %-30s\n" \ + "$pid" "DEAD" "orphaned" "$IN_NSMI" "-" "❌ /proc 없음 — Pod 추적 불가" + continue + fi + + STATE=$(cat /proc/$pid/status 2>/dev/null | grep "^State:" | awk '{print $2}') + + GPU_MEM=$(nvidia-smi --query-compute-apps=pid,used_gpu_memory \ + --format=csv,noheader,nounits 2>/dev/null | grep "^${pid}," | awk -F', ' '{print $2}' | xargs) + [ -z "$GPU_MEM" ] && GPU_MEM="fd_only" + + CONTAINER_ID=$(extract_container_id $pid) + QOS=$(extract_qos $pid) + + if [ -z "$CONTAINER_ID" ]; then + CMD=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | head -c 40) + printf "%-8s %-6s %-12s %-8s %-12s %-30s\n" \ + "$pid" "$STATE" "${GPU_MEM}" "$IN_NSMI" "$QOS" "호스트: $CMD" + continue + fi + + POD_INFO=$(get_pod_info "$CONTAINER_ID") + + if [ -n "$POD_INFO" ]; then + POD_NAME=$(echo "$POD_INFO" | cut -d'|' -f1) + NAMESPACE=$(echo "$POD_INFO" | cut -d'|' -f2) + CONT_NAME=$(echo "$POD_INFO" | cut -d'|' -f3) + printf "%-8s %-6s %-12s %-8s %-12s %-30s %-20s %-15s\n" \ + "$pid" "$STATE" "${GPU_MEM}" "$IN_NSMI" "$QOS" "$POD_NAME" "$NAMESPACE" "$CONT_NAME" + else + SHORT_ID=$(echo "$CONTAINER_ID" | head -c 12) + POD_UID=$(extract_pod_uid $pid) + printf "%-8s %-6s %-12s %-8s %-12s %-30s\n" \ + "$pid" "$STATE" "${GPU_MEM}" "$IN_NSMI" "$QOS" "⚠️ 삭제됨 (ctr:${SHORT_ID} pod:${POD_UID})" + fi +done + +echo "--------------------------------------------------------------------------------------------------------------" + +# --- 3단계: 숨겨진 프로세스 --- + +HIDDEN_PIDS=$(comm -13 <(echo "$NVIDIA_PIDS" | sort) <(echo "$FUSER_PIDS" | sort) 2>/dev/null | grep -v '^$') +if [ -n "$HIDDEN_PIDS" ]; then + echo "" + echo "[3] ⚠️ nvidia-smi 미등록 but GPU fd 보유 프로세스:" + for pid in $HIDDEN_PIDS; do + if [ -d "/proc/$pid" ]; then + CMD=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | head -c 60) + FDS=$(ls -la /proc/$pid/fd 2>/dev/null | grep nvidia | wc -l) + CONTAINER_ID=$(extract_container_id $pid) + if [ -n "$CONTAINER_ID" ]; then + POD_INFO=$(get_pod_info "$CONTAINER_ID") + POD_NAME=$(echo "$POD_INFO" | cut -d'|' -f1) + echo " PID $pid: nvidia fd ${FDS}개, Pod: ${POD_NAME:-unknown}, CMD: $CMD" + else + echo " PID $pid: nvidia fd ${FDS}개, 호스트 프로세스, CMD: $CMD" + fi + fi + done +fi + +# --- 4단계: 요약 --- + +echo "" +echo "[4] 요약" +TOTAL=$(echo "$ALL_PIDS" | grep -v '^$' | wc -l) +DEAD=0; ZOMBIE=0; HIDDEN_COUNT=0 +for pid in $ALL_PIDS; do + [ ! -d "/proc/$pid" ] && DEAD=$((DEAD+1)) + [ -d "/proc/$pid" ] && grep -q "^State:.*Z" /proc/$pid/status 2>/dev/null && ZOMBIE=$((ZOMBIE+1)) +done +[ -n "$HIDDEN_PIDS" ] && HIDDEN_COUNT=$(echo "$HIDDEN_PIDS" | grep -v '^$' | wc -l) + +echo " 전체 GPU 관련 프로세스: $TOTAL" +echo " 정상: $((TOTAL - DEAD - ZOMBIE))" +echo " 죽은 프로세스 (orphaned ctx): $DEAD" +echo " 좀비 프로세스: $ZOMBIE" +echo " nvidia-smi 미표시: $HIDDEN_COUNT" + +if [ "$DEAD" -gt 0 ] || [ "$ZOMBIE" -gt 0 ]; then + echo "" + echo " ⚠️ 조치 필요:" + [ "$ZOMBIE" -gt 0 ] && echo " 좀비 → 부모 프로세스 kill 또는 Pod 재시작" + [ "$DEAD" -gt 0 ] && echo " orphaned ctx → Pod 재시작, 안 되면 nvidia-smi --gpu-reset 또는 노드 재부팅" +fi + +echo "" +echo "============================================" +``` + +--- + +## Phase 0: CI/CD 환경 구성 + +수동 `docker build/push` + `kubectl apply` 흐름을 **Gitea Actions + Harbor + ArgoCD**로 자동화한다. +이 Phase 완료 후, Phase 1~2-C의 "이미지 빌드 & 푸시" 단계는 CI가 자동 수행하며, +Pod 배포는 ArgoCD가 Harbor OCI Helm chart에서 sync한다. +(아래 본문에 남아 있는 수동 `docker build/push` 명령은 CI fallback 용도로 보존.) + +### 환경 정보 (`vLLM/A100_npu/.env`에서 로드) + +| 항목 | 값 | 비고 | +|---|---|---| +| Gitea URL | `https://gitea.inje-private.com` | | +| Gitea Org | `soo-rnd` | Runner 연동 완료 | +| Gitea Repo | `gpu-zombie-test` | **신규 생성 필요** | +| Harbor URL | `harbor.cone-chain.net` | OIDC CLI Secret 사용 | +| Harbor Project | `aipf` | 기존 재사용 | +| 이미지 prefix | `harbor.cone-chain.net/aipf/gpu-zombie-test` | tag로 시나리오 구분 | +| Helm Chart | `gpu-zombie-test` | OCI push | +| ArgoCD URL | `https://argocd.gpulive.cloud` | | +| ArgoCD App | `gpu-zombie-test` | dest namespace `gpu-zombie-test` | +| GPU 노드 | `gpu-4` (A100) | nodeName으로 강제 | +| K8s Pull Secret | `harbor-pull-secret` | namespace에 사전 생성 | + +> **즉, 본문의 `harbor.cone-chain.net/aipf/gpu-zombie-test:` 는 모두 `harbor.cone-chain.net/aipf/gpu-zombie-test:` 로 매핑된다.** +> 이미지 태그: `nofix` / `tini` / `fullfix` (시나리오 2·3 동일 이미지) + +### 0-1. Gitea Repo & Secrets 준비 + +```bash +# 1. soo-rnd/gpu-zombie-test repo 생성 (Gitea UI 또는 API) + +# 2. Gitea repo Settings → Actions → Secrets 등록 +# REGISTRY_URL = harbor.cone-chain.net +# REGISTRY_USER = selee +# REGISTRY_PASSWORD = # .env의 REGISTRY_PASSWORD +# ARGOCD_URL = https://argocd.gpulive.cloud +# ARGOCD_USER = admin +# ARGOCD_PASSWORD = # .env의 ARGOCD_ADMIN_PASSWORD + +# 3. K8s namespace + pull secret 사전 생성 (gpu-4 클러스터) +kubectl create ns gpu-zombie-test +kubectl create secret docker-registry harbor-pull-secret \ + --docker-server=harbor.cone-chain.net \ + --docker-username=selee \ + --docker-password='' \ + -n gpu-zombie-test +``` + +### 0-2. Repo 디렉토리 구조 (CI/CD 추가분) + +기존 시나리오 디렉토리(plan.md 상단의 구조도)에 더해: + +``` +gpu-zombie-test/ +├── .gitea/ +│ └── workflows/ +│ ├── ci.yaml # 시나리오 이미지 빌드/푸시 +│ └── cd.yaml # Helm chart 패키징/푸시 +├── charts/ +│ └── gpu-zombie-test/ +│ ├── Chart.yaml +│ ├── values.yaml # scenario 토글, image tag +│ └── templates/ +│ ├── _helpers.tpl +│ ├── pod-nofix.yaml +│ ├── pod-tini.yaml +│ ├── pod-tini-prestop.yaml +│ └── pod-fullfix.yaml +├── argocd-app.yaml # ArgoCD Application 정의 +├── scenario-1-nofix/ ... # 기존 +├── scenario-2-tini-only/ ... +├── scenario-3-tini-prestop/ ... +├── scenario-4-fullfix/ ... +├── scripts/ ... +└── results/ ... +``` + +### 0-3. `.gitea/workflows/ci.yaml` — 이미지 빌드 & Harbor push + +시나리오 2·3은 동일 이미지(`tini`)이므로 총 **3개 이미지** 빌드. + +```yaml +name: CI — Build & Push Scenario Images + +on: + push: + branches: [main] + paths: + - 'scenario-*/Dockerfile' + - 'scenario-*/parent.py' + - 'scenario-*/gpu_worker*.py' + - 'scripts/observe.sh' + - '.gitea/workflows/ci.yaml' + +jobs: + build-and-push: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - scenario: scenario-1-nofix + tag: nofix + - scenario: scenario-2-tini-only + tag: tini + - scenario: scenario-4-fullfix + tag: fullfix + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set image tag + run: | + SHORT_SHA=$(echo ${GITHUB_SHA} | cut -c1-7) + echo "SHA_TAG=${{ matrix.tag }}-sha-${SHORT_SHA}" >> $GITHUB_ENV + + - name: Login to Harbor + run: | + for i in 1 2 3; do + echo '${{ secrets.REGISTRY_PASSWORD }}' | \ + docker login ${{ secrets.REGISTRY_URL }} \ + -u ${{ secrets.REGISTRY_USER }} --password-stdin && break + echo "Retry $i..."; sleep 5 + done + + - name: Prepare build context (observe.sh + prestop.sh 포함) + run: | + cp scripts/observe.sh ${{ matrix.scenario }}/observe.sh + # prestop.sh는 시나리오 2(=3 공용)·4 이미지에만 필요하지만, + # 모든 컨텍스트에 복사해두고 Dockerfile COPY 여부로 제어 (nofix는 미사용) + cp scripts/prestop.sh ${{ matrix.scenario }}/prestop.sh + + - name: Build & push image + run: | + IMAGE=${{ secrets.REGISTRY_URL }}/aipf/gpu-zombie-test + docker build \ + -t ${IMAGE}:${{ env.SHA_TAG }} \ + -t ${IMAGE}:${{ matrix.tag }} \ + ${{ matrix.scenario }} + docker push ${IMAGE}:${{ env.SHA_TAG }} + docker push ${IMAGE}:${{ matrix.tag }} +``` + +### 0-4. `.gitea/workflows/cd.yaml` — Helm chart 패키징 & Harbor OCI push + +```yaml +name: CD — Package & Deploy Helm Chart + +on: + push: + tags: ['v*'] + +env: + CHART_NAME: gpu-zombie-test + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set versions + run: | + TAG=${GITHUB_REF#refs/tags/} + echo "TAG=${TAG}" >> $GITHUB_ENV + echo "CHART_VERSION=${TAG#v}" >> $GITHUB_ENV + + - name: Install Helm + run: | + curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + + - name: Package Helm chart + run: | + sed -i "s/appVersion: .*/appVersion: \"${{ env.TAG }}\"/" \ + charts/${CHART_NAME}/Chart.yaml + sed -i "s/^version: .*/version: ${{ env.CHART_VERSION }}/" \ + charts/${CHART_NAME}/Chart.yaml + helm package charts/${CHART_NAME} + + - name: Push Helm chart to Harbor (OCI) + run: | + echo '${{ secrets.REGISTRY_PASSWORD }}' | \ + helm registry login ${{ secrets.REGISTRY_URL }} \ + -u ${{ secrets.REGISTRY_USER }} --password-stdin + helm push ${CHART_NAME}-${{ env.CHART_VERSION }}.tgz \ + oci://${{ secrets.REGISTRY_URL }}/aipf +``` + +### 0-5. Helm Chart — `charts/gpu-zombie-test/` + +**Chart.yaml** +```yaml +apiVersion: v2 +name: gpu-zombie-test +description: GPU zombie/orphan/orphaned-CUDA-context reproduction experiment +type: application +version: 0.1.0 +appVersion: "v0.1.0" +``` + +**values.yaml** +```yaml +image: + repository: harbor.cone-chain.net/aipf/gpu-zombie-test + pullPolicy: Always + pullSecret: harbor-pull-secret + +node: gpu-4 + +# 시나리오별 토글 — 실험 격리가 필요하면 false 처리 후 chart 버전 bump +scenarios: + nofix: + enabled: true + tag: nofix + gracePeriod: 30 + tini: + enabled: true + tag: tini + gracePeriod: 30 + tiniPrestop: + enabled: true + tag: tini # scenario-2와 동일 이미지 + gracePeriod: 60 + fullfix: + enabled: true + tag: fullfix + gracePeriod: 90 +``` + +**templates/_helpers.tpl** +```yaml +{{- define "gpu-zombie.podSpec" -}} +nodeName: {{ .root.Values.node }} +terminationGracePeriodSeconds: {{ .scenario.gracePeriod }} +restartPolicy: Never +imagePullSecrets: + - name: {{ .root.Values.image.pullSecret }} +containers: + - name: worker + image: "{{ .root.Values.image.repository }}:{{ .scenario.tag }}" + imagePullPolicy: {{ .root.Values.image.pullPolicy }} + resources: + limits: + nvidia.com/gpu: 1 +{{- end }} +``` + +**templates/pod-nofix.yaml** +```yaml +{{- if .Values.scenarios.nofix.enabled }} +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-nofix + labels: + app: gpu-zombie-test + scenario: nofix +spec: + {{- include "gpu-zombie.podSpec" (dict "root" . "scenario" .Values.scenarios.nofix) | nindent 2 }} +{{- end }} +``` + +**templates/pod-tini.yaml** +```yaml +{{- if .Values.scenarios.tini.enabled }} +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-tini + labels: + app: gpu-zombie-test + scenario: tini +spec: + {{- include "gpu-zombie.podSpec" (dict "root" . "scenario" .Values.scenarios.tini) | nindent 2 }} +{{- end }} +``` + +**templates/pod-tini-prestop.yaml** — preStop은 inline이 길어 helper 분리 +```yaml +{{- if .Values.scenarios.tiniPrestop.enabled }} +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-tini-prestop + labels: + app: gpu-zombie-test + scenario: tini-prestop +spec: + nodeName: {{ .Values.node }} + terminationGracePeriodSeconds: {{ .Values.scenarios.tiniPrestop.gracePeriod }} + restartPolicy: Never + imagePullSecrets: + - name: {{ .Values.image.pullSecret }} + containers: + - name: worker + image: "{{ .Values.image.repository }}:{{ .Values.scenarios.tiniPrestop.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: + limits: + nvidia.com/gpu: 1 + lifecycle: + preStop: + exec: + # 공용 prestop.sh — 이미지에 포함됨 (scripts/prestop.sh 참조) + command: ["/bin/sh", "-c", "/usr/local/bin/prestop.sh"] +{{- end }} +``` + +**templates/pod-fullfix.yaml** — 동일 패턴 + Phase 2-C의 preStop +```yaml +{{- if .Values.scenarios.fullfix.enabled }} +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-fullfix + labels: + app: gpu-zombie-test + scenario: fullfix +spec: + nodeName: {{ .Values.node }} + terminationGracePeriodSeconds: {{ .Values.scenarios.fullfix.gracePeriod }} + restartPolicy: Never + imagePullSecrets: + - name: {{ .Values.image.pullSecret }} + containers: + - name: worker + image: "{{ .Values.image.repository }}:{{ .Values.scenarios.fullfix.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: + limits: + nvidia.com/gpu: 1 + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "/usr/local/bin/prestop.sh"] +{{- end }} +``` + +> **메모:** 시나리오 3·4의 preStop은 공용 스크립트 `scripts/prestop.sh` (위 "공통 스크립트" 섹션 참조)를 +> 사용한다. CI가 빌드 컨텍스트에 복사하고, 각 Dockerfile이 `/usr/local/bin/prestop.sh`로 COPY +chmod 한다. +> Pod template은 단지 `command: ["/bin/sh", "-c", "/usr/local/bin/prestop.sh"]`만 호출한다. + +### 0-6. `argocd-app.yaml` + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gpu-zombie-test + namespace: argocd +spec: + project: default + source: + chart: gpu-zombie-test + repoURL: harbor.cone-chain.net/aipf + targetRevision: ">0.0.0" + helm: + releaseName: gpu-zombie-test + destination: + server: https://kubernetes.default.svc + namespace: gpu-zombie-test + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true +``` + +ArgoCD에 한 번만 등록: +```bash +kubectl apply -f argocd-app.yaml +``` + +이후 Harbor OCI에 새 chart 버전이 push될 때마다 자동 sync (`targetRevision: ">0.0.0"`). + +### 0-7. CI/CD 트리거 전략 & 전체 흐름 + +``` +[코드 변경] + ├─ scenario-*/ 변경 → CI 트리거 (push to main) + │ └─ matrix로 3개 이미지 build → Harbor push + │ (harbor.cone-chain.net/aipf/gpu-zombie-test:{nofix|tini|fullfix}) + │ + sha-<7> 태그 동시 push + │ + └─ git tag v0.1.x push → CD 트리거 + └─ Helm chart package → Harbor OCI push + └─ ArgoCD 자동 sync (targetRevision: ">0.0.0") + └─ gpu-zombie-test ns에 4개 Pod 배포 +``` + +**시나리오 실험 사이클 (Phase 1~2-C에서 본문 대신 이걸로 대체):** + +```bash +# 1. 코드 수정 후 push → CI 자동 빌드 확인 (Gitea Actions UI) +git add scenario-1-nofix && git commit -m "fix: nofix worker" && git push + +# 2. ArgoCD가 이미지 변경 감지하지 않으므로, 첫 배포 또는 chart 변경 시에만 sync +# 이미 같은 tag(:nofix)에 새 이미지가 들어왔으면 Pod 재생성으로 pull +kubectl delete pod gpu-zombie-nofix -n gpu-zombie-test +# → ArgoCD selfHeal로 재배포 + imagePullPolicy: Always 로 새 이미지 pull + +# 3. Pod 진입해서 실험 +kubectl exec -it gpu-zombie-nofix -n gpu-zombie-test -- bash + +# 4. 시나리오 격리 실행이 필요할 때 (옵션): +# values.yaml에서 다른 scenario.enabled: false 처리 → chart version bump → tag push +# → ArgoCD가 비활성 Pod prune +``` + +### 0-8. 체크리스트 — Phase 0 + +- [ ] Gitea `soo-rnd/gpu-zombie-test` repo 생성 +- [ ] Gitea repo Secrets 등록 (`REGISTRY_URL`, `REGISTRY_USER`, `REGISTRY_PASSWORD`) +- [ ] K8s `gpu-zombie-test` namespace 생성 +- [ ] `harbor-pull-secret` 생성 (위 namespace) +- [ ] `.gitea/workflows/ci.yaml`, `cd.yaml` 작성 & 첫 push로 CI 동작 확인 +- [ ] Harbor에 3개 이미지 (`:nofix`, `:tini`, `:fullfix`) 존재 확인 +- [ ] `scripts/prestop.sh` 작성 (시나리오 3·4 공용) +- [ ] `charts/gpu-zombie-test/` 작성 +- [ ] 첫 tag `v0.1.0` push → CD 동작 확인 +- [ ] Harbor OCI에 chart 존재 확인 (`helm pull oci://harbor.cone-chain.net/aipf/gpu-zombie-test --version 0.1.0`) +- [ ] `argocd-app.yaml` apply & ArgoCD UI에서 Sync OK 확인 +- [ ] `kubectl get pods -n gpu-zombie-test` → 4개 Pod Running 확인 +- [ ] 4개 Pod에 대해 `kubectl exec`로 진입 가능 확인 + +--- + +## Phase 1: 문제 재현 (시나리오 1 — No Fix) + +### 목표 +PID 1이 bash인 컨테이너에서 좀비 프로세스, 고아 프로세스, orphaned GPU context를 모두 재현한다. + +### 파일 + +**scenario-1-nofix/parent.py** +```python +import subprocess +import time +import os + +print(f"[Parent] PID: {os.getpid()}, PPID: {os.getppid()}") + +# 자식 프로세스 spawn (GPU를 점유할 worker) +child = subprocess.Popen(["python3", "gpu_worker.py"]) +print(f"[Parent] Child spawned, PID: {child.pid}") + +# 부모가 wait() 없이 종료 → 자식은 고아가 됨 +time.sleep(5) +print("[Parent] wait() 없이 종료합니다") +os._exit(0) +``` + +**scenario-1-nofix/gpu_worker.py** +```python +import torch +import time +import os +import signal + +# SIGTERM 무시 — 잘못 작성된 코드 시뮬레이션 +signal.signal(signal.SIGTERM, signal.SIG_IGN) + +print(f"[Worker] PID: {os.getpid()}, PPID: {os.getppid()}") + +# GPU 메모리 할당 (~2GB) +device = torch.device('cuda:0') +tensors = [] +for i in range(8): + t = torch.randn(1024, 1024, 64, device=device) + tensors.append(t) + +allocated = torch.cuda.memory_allocated() / 1024**3 +print(f"[Worker] GPU 메모리 할당 완료: {allocated:.2f} GB") + +# 무한 대기 +while True: + time.sleep(5) + print(f"[Worker] alive, PID={os.getpid()}, PPID={os.getppid()}") +``` + +**scenario-1-nofix/Dockerfile** +```dockerfile +FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 + +RUN apt-get update && apt-get install -y python3 python3-pip procps psmisc && \ + pip3 install torch --break-system-packages + +WORKDIR /app +COPY parent.py gpu_worker.py ./ +COPY observe.sh /usr/local/bin/observe.sh +RUN chmod +x /usr/local/bin/observe.sh + +# PID 1 = bash (좀비 수거 안 함) +ENTRYPOINT ["/bin/bash", "-c"] +CMD ["sleep infinity"] +``` + +**scenario-1-nofix/pod.yaml** +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-nofix + namespace: gpu-zombie-test +spec: + nodeName: gpu-4 + terminationGracePeriodSeconds: 30 + containers: + - name: worker + image: harbor.cone-chain.net/aipf/gpu-zombie-test:nofix + resources: + limits: + nvidia.com/gpu: 1 + restartPolicy: Never +``` + +### 실행 절차 + +```bash +# 1. 이미지 빌드 & 푸시 +cd scenario-1-nofix +cp ../scripts/observe.sh . +docker build -t harbor.cone-chain.net/aipf/gpu-zombie-test:nofix . +docker push harbor.cone-chain.net/aipf/gpu-zombie-test:nofix + +# 2. 파드 배포 +kubectl apply -f pod.yaml + +# 3. 파드 진입 +kubectl exec -it gpu-zombie-nofix -n gpu-zombie-test -- bash + +# 4. PID 1 확인 +cat /proc/1/cmdline | tr '\0' ' ' +# 예상: /bin/bash -c sleep infinity + +# 5. parent.py 실행 (백그라운드) +python3 parent.py & + +# 6. 10초 대기 (부모 종료 대기) +sleep 10 + +# 7. 컨테이너 내부 관찰 +observe.sh + +# 8. 노드에서 역추적 (별도 터미널) +ssh gpu-4 +bash gpu-pod-trace.sh +``` + +### 체크리스트 — 시나리오 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 매핑 확인 + +**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 상태 확인 + +**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/`) + +**다음 시나리오 전 초기화:** +- [ ] `nvidia-smi`로 GPU 메모리 0 확인 +- [ ] 잔존 시 `nvidia-smi --gpu-reset` 시도 +- [ ] 안 되면 gpu-4 노드 재부팅 → GPU 정상 확인 + +--- + +## Phase 2-A: tini만 적용 (시나리오 2) + +### 목표 +tini를 PID 1로 사용하면 좀비 수거는 되지만, 코드에 signal handler가 없으면 CUDA cleanup이 안 되는 것을 확인한다. + +### 변경 사항 (시나리오 1 대비) +- Dockerfile: `ENTRYPOINT ["tini", "--"]`로 변경 +- gpu_worker.py: **동일** (SIGTERM 무시) +- pod.yaml: preStop 없음 + +**scenario-2-tini-only/Dockerfile** + +> 이 이미지(`:tini`)는 시나리오 3에서도 재사용되므로 `prestop.sh`를 미리 포함시킨다. +> 시나리오 2는 호출하지 않으니 동작에 영향 없음. + +```dockerfile +FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 + +RUN apt-get update && apt-get install -y python3 python3-pip procps psmisc tini && \ + pip3 install torch --break-system-packages + +WORKDIR /app +COPY parent.py gpu_worker.py ./ +COPY observe.sh /usr/local/bin/observe.sh +COPY prestop.sh /usr/local/bin/prestop.sh +RUN chmod +x /usr/local/bin/observe.sh /usr/local/bin/prestop.sh + +# PID 1 = tini (좀비 수거 O, signal 전달 O) +ENTRYPOINT ["tini", "--"] +CMD ["sleep", "infinity"] +``` + +**scenario-2-tini-only/pod.yaml** +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-tini + namespace: gpu-zombie-test +spec: + nodeName: gpu-4 + terminationGracePeriodSeconds: 30 + containers: + - name: worker + image: harbor.cone-chain.net/aipf/gpu-zombie-test:tini + resources: + limits: + nvidia.com/gpu: 1 + restartPolicy: Never +``` + +### 실행 절차 + +```bash +cd scenario-2-tini-only +cp ../scripts/observe.sh . +docker build -t harbor.cone-chain.net/aipf/gpu-zombie-test:tini . +docker push harbor.cone-chain.net/aipf/gpu-zombie-test:tini +kubectl apply -f pod.yaml + +kubectl exec -it gpu-zombie-tini -n gpu-zombie-test -- bash +python3 parent.py & +sleep 10 +observe.sh + +# 노드에서 +ssh gpu-4 +bash gpu-pod-trace.sh +``` + +### 체크리스트 — 시나리오 2 + +**tini 효과 확인:** +- [ ] PID 1이 `tini`인 것 확인 +- [ ] parent.py 종료 후 좀비가 **수거되는지** 확인 (시나리오 1과 비교) +- [ ] gpu_worker.py 고아 상태 확인 + +**CUDA cleanup 안 되는 것 확인:** +- [ ] worker에 SIGTERM 전송: `kill -TERM ` +- [ ] worker가 SIGTERM 무시하는지 확인 (프로세스 살아있음) +- [ ] `kill -9 ` 후 orphaned GPU context 발생 여부 확인 +- [ ] gpu-pod-trace.sh로 상태 확인 + +**Pod 삭제 테스트:** +- [ ] Pod 삭제 후 GPU 메모리 해제 여부 기록 +- [ ] 결과 저장 (`results/scenario-2/`) + +**예상 결과:** +- 좀비 수거: ✅ (tini가 `waitpid(-1)` 호출) +- CUDA cleanup: ❌ (worker 코드에 signal handler 없음) +- kill -9 시 orphaned context: 발생 가능 + +**다음 시나리오 전 초기화:** +- [ ] GPU 메모리 0 확인, 필요 시 gpu-4 재부팅 + +--- + +## Phase 2-B: tini + preStop 적용 (시나리오 3) + +### 목표 +preStop hook이 SIGTERM을 보내도, 코드가 SIGTERM을 무시하면 결국 SIGKILL fallback → CUDA cleanup 불가를 확인한다. + +### 변경 사항 (시나리오 2 대비) +- Dockerfile: 동일 (tini 이미지 재사용) +- gpu_worker.py: **동일** (SIGTERM 무시) +- pod.yaml: **preStop hook 추가**, gracePeriod 60초 + +**scenario-3-tini-prestop/pod.yaml** +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-tini-prestop + namespace: gpu-zombie-test +spec: + nodeName: gpu-4 + terminationGracePeriodSeconds: 60 + containers: + - name: worker + image: harbor.cone-chain.net/aipf/gpu-zombie-test:tini # 시나리오 2와 동일 이미지 + resources: + limits: + nvidia.com/gpu: 1 + lifecycle: + preStop: + exec: + # 공용 prestop.sh 호출 (이미지에 /usr/local/bin/prestop.sh로 포함됨) + # 동작: SIGTERM → 20초 대기 → SIGKILL fallback + # 시나리오 3는 worker가 SIGTERM 무시하므로 fallback 경로 트리거됨 + command: ["/bin/sh", "-c", "/usr/local/bin/prestop.sh"] + restartPolicy: Never +``` + +### 실행 절차 + +```bash +# 이미지는 시나리오 2와 동일 (prestop.sh 포함됨) +kubectl apply -f pod.yaml + +kubectl exec -it gpu-zombie-tini-prestop -n gpu-zombie-test -- bash +python3 parent.py & +sleep 10 +observe.sh + +# Pod 삭제 (preStop 동작 확인) +# 터미널 1 (gpu-4): watch -n 1 'nvidia-smi' +# 터미널 2: 로그 스트림 + 삭제 +kubectl logs -f gpu-zombie-tini-prestop -n gpu-zombie-test & +kubectl delete pod gpu-zombie-tini-prestop -n gpu-zombie-test + +# 노드에서 확인 +ssh gpu-4 +bash gpu-pod-trace.sh +``` + +### 체크리스트 — 시나리오 3 + +**preStop 동작 확인:** +- [ ] preStop 실행 확인 (로그에서 `[preStop]` 메시지) +- [ ] preStop이 SIGTERM 전송 확인 +- [ ] worker가 SIGTERM 무시 → 20초 대기 → SIGKILL fallback 확인 +- [ ] SIGKILL 후 GPU 메모리 상태 확인 + +**Pod 삭제 후:** +- [ ] gpu-pod-trace.sh로 GPU 상태 확인 +- [ ] orphaned GPU context 발생 여부 기록 +- [ ] 결과 저장 (`results/scenario-3/`) + +**예상 결과:** +- 좀비 수거: ✅ (tini) +- preStop SIGTERM 전달: ✅ +- CUDA cleanup: ❌ (코드가 SIGTERM 무시 → SIGKILL) +- orphaned context: 발생 가능 + +**다음 시나리오 전 초기화:** +- [ ] GPU 메모리 0 확인, 필요 시 gpu-4 재부팅 + +--- + +## Phase 2-C: 전체 적용 (시나리오 4 — Full Fix) + +### 목표 +tini + preStop + 코드 signal handler + gracePeriod 모두 적용 시 좀비, 고아, orphaned GPU context가 깔끔하게 정리되는 것을 확인한다. + +### 변경 사항 +- Dockerfile: tini (동일) +- gpu_worker_fixed.py: **SIGTERM handler + CUDA cleanup** ← 핵심 변경 +- pod.yaml: preStop + gracePeriod 90초 + +**scenario-4-fullfix/gpu_worker_fixed.py** +```python +import torch +import time +import os +import signal +import sys + +print(f"[Worker] PID: {os.getpid()}, PPID: {os.getppid()}") + +# GPU 메모리 할당 (~2GB) +device = torch.device('cuda:0') +tensors = [] +for i in range(8): + t = torch.randn(1024, 1024, 64, device=device) + tensors.append(t) + +allocated = torch.cuda.memory_allocated() / 1024**3 +print(f"[Worker] GPU 메모리 할당 완료: {allocated:.2f} GB") + +# === 핵심: SIGTERM handler 등록 === +def graceful_shutdown(signum, frame): + print(f"[Worker] Signal {signum} 수신, CUDA cleanup 시작...") + + # 1. tensor 참조 해제 + tensors.clear() + + # 2. GPU 캐시 비우기 + torch.cuda.empty_cache() + + # 3. 동기화 후 종료 + torch.cuda.synchronize() + + after = torch.cuda.memory_allocated() / 1024**3 + print(f"[Worker] cleanup 완료, 잔여 GPU 메모리: {after:.2f} GB") + + sys.exit(0) + +signal.signal(signal.SIGTERM, graceful_shutdown) +signal.signal(signal.SIGINT, graceful_shutdown) + +# 무한 대기 +while True: + time.sleep(5) + print(f"[Worker] alive, PID={os.getpid()}, PPID={os.getppid()}, " + f"GPU={torch.cuda.memory_allocated()/1024**3:.2f}GB") +``` + +**scenario-4-fullfix/Dockerfile** +```dockerfile +FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 + +RUN apt-get update && apt-get install -y python3 python3-pip procps psmisc tini && \ + pip3 install torch --break-system-packages + +WORKDIR /app +COPY parent.py gpu_worker_fixed.py ./ +COPY observe.sh /usr/local/bin/observe.sh +COPY prestop.sh /usr/local/bin/prestop.sh +RUN chmod +x /usr/local/bin/observe.sh /usr/local/bin/prestop.sh + +ENTRYPOINT ["tini", "--"] +CMD ["sleep", "infinity"] +``` + +**scenario-4-fullfix/pod.yaml** +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-fullfix + namespace: gpu-zombie-test +spec: + nodeName: gpu-4 + terminationGracePeriodSeconds: 90 + containers: + - name: worker + image: harbor.cone-chain.net/aipf/gpu-zombie-test:fullfix + resources: + limits: + nvidia.com/gpu: 1 + lifecycle: + preStop: + exec: + # 공용 prestop.sh 호출 — 시나리오 3과 동일 스크립트 + # fullfix는 worker가 SIGTERM을 graceful하게 처리하므로 + # SIGKILL fallback 경로는 트리거되지 않음 + command: ["/bin/sh", "-c", "/usr/local/bin/prestop.sh"] + restartPolicy: Never +``` + +### 실행 절차 + +```bash +cd scenario-4-fullfix +cp ../scripts/observe.sh . +docker build -t harbor.cone-chain.net/aipf/gpu-zombie-test:fullfix . +docker push harbor.cone-chain.net/aipf/gpu-zombie-test:fullfix +kubectl apply -f pod.yaml + +kubectl exec -it gpu-zombie-fullfix -n gpu-zombie-test -- bash +python3 parent.py & +sleep 10 +observe.sh + +# 수동 SIGTERM 테스트 +kill -TERM +sleep 3 +observe.sh +# → GPU 메모리 해제 확인 + +# Pod 삭제 테스트 +kubectl logs -f gpu-zombie-fullfix -n gpu-zombie-test & +kubectl delete pod gpu-zombie-fullfix -n gpu-zombie-test + +# 노드에서 확인 +ssh gpu-4 +bash gpu-pod-trace.sh +``` + +### 체크리스트 — 시나리오 4 + +**Graceful shutdown 확인:** +- [ ] PID 1이 tini인 것 확인 +- [ ] parent.py 종료 후 좀비 수거 확인 +- [ ] worker에 SIGTERM → `[Worker] Signal 15 수신, CUDA cleanup 시작...` 로그 +- [ ] `torch.cuda.empty_cache()` + `synchronize()` 실행 확인 +- [ ] SIGTERM 후 `nvidia-smi` → GPU 메모리 **해제됨** 확인 +- [ ] orphaned GPU context **없음** 확인 +- [ ] fuser로도 GPU fd 잡는 프로세스 **없음** 확인 + +**Pod 삭제 시 전체 흐름:** +- [ ] preStop → SIGTERM → worker cleanup → 정상 종료 로그 확인 +- [ ] Pod 삭제 후 GPU 메모리 **완전 해제** 확인 +- [ ] gpu-pod-trace.sh → GPU 관련 프로세스 **0개** 확인 +- [ ] 결과 저장 (`results/scenario-4/`) + +**예상 결과:** +- 좀비 수거: ✅ (tini) +- SIGTERM 전달: ✅ (tini + preStop) +- CUDA cleanup: ✅ (코드에 signal handler) +- orphaned context: **없음** +- GPU 메모리 해제: ✅ + +--- + +## Phase 3: 결과 비교 + +### 최종 비교표 + +| 항목 | S1 (No Fix) | S2 (tini) | S3 (tini+preStop) | S4 (Full Fix) | +|---|---|---|---|---| +| PID 1 | bash | tini | tini | tini | +| 좀비 수거 | ❌ | ✅ | ✅ | ✅ | +| 고아 GPU 점유 | ❌ 잔존 | ❌ 잔존 | ⚠️ SIGKILL fallback | ✅ graceful | +| SIGTERM → CUDA cleanup | ❌ | ❌ handler 없음 | ❌ handler 없음 | ✅ | +| kill -9 후 orphaned ctx | ❌ 발생 | ❌ 발생 | ❌ 발생 가능 | N/A | +| nvidia-smi 미표시 ctx | 미확인 | 미확인 | 미확인 | 없음 | +| Pod 삭제 후 GPU 메모리 | ❌ 잔존 가능 | ⚠️ 불확실 | ⚠️ 불확실 | ✅ 해제 | +| PID → Pod 추적 (trace) | 기록 | 기록 | 기록 | 깨끗 | +| 노드 재부팅 필요 | 가능 | 가능 | 가능 | 불필요 | + +### 각 시나리오별 결과 저장 구조 + +``` +results/ +├── scenario-1/ +│ ├── observe-after-spawn.log # parent.py 실행 후 observe.sh +│ ├── observe-after-kill.log # kill -9 후 observe.sh +│ ├── gpu-pod-trace.log # 노드에서 gpu-pod-trace.sh +│ ├── nvidia-smi-before-delete.log +│ ├── nvidia-smi-after-delete.log +│ └── notes.md +├── scenario-2/ +│ └── ... +├── scenario-3/ +│ └── ... +├── scenario-4/ +│ └── ... +└── comparison.md # 최종 비교 분석 +``` + +--- + +## 전체 실행 순서 체크리스트 + +### 사전 준비 +- [ ] gpu-4 노드 SSH 접근 확인 +- [ ] gpu-4 GPU 정상 확인 (`nvidia-smi`) +- [ ] gpu-4에 `crictl`, `fuser`, `jq` 설치 확인 +- [ ] gpu-4에 `gpu-pod-trace.sh`, `node-observe.sh` 배포 +- [ ] cgroup v1 형식 재확인 (`cat /proc//cgroup`) +- [ ] namespace `gpu-zombie-test` 생성 +- [ ] 컨테이너 레지스트리 접근 확인 +- [ ] `results/` 디렉토리 구조 생성 + +### Phase 0: CI/CD 환경 구성 +- [ ] Gitea `soo-rnd/gpu-zombie-test` repo 생성 + Secrets 등록 +- [ ] K8s `harbor-pull-secret` 생성 +- [ ] `.gitea/workflows/{ci,cd}.yaml` 작성, 첫 push로 CI 동작 확인 +- [ ] Harbor에 3개 이미지 (`:nofix`, `:tini`, `:fullfix`) 빌드 확인 +- [ ] `scripts/prestop.sh` 작성 (시나리오 3·4 공용 — Dockerfile에서 `/usr/local/bin/`으로 COPY) +- [ ] `charts/gpu-zombie-test/` 작성 (4개 pod template + helper) +- [ ] tag `v0.1.0` push → CD 동작 확인 → Harbor OCI chart push 확인 +- [ ] `argocd-app.yaml` apply → ArgoCD UI Sync OK 확인 +- [ ] `kubectl get pods -n gpu-zombie-test` → 4개 Pod Running 확인 + +### 시나리오 1: No Fix +- [ ] 이미지 빌드 & 푸시 +- [ ] Pod 배포 & 진입 +- [ ] 좀비/고아/orphaned context 재현 +- [ ] gpu-pod-trace.sh로 PID → Pod 매핑 확인 +- [ ] kill -9 후 orphaned context 확인 +- [ ] fuser로 nvidia-smi 미표시 context 확인 +- [ ] Pod 삭제 후 GPU 상태 기록 +- [ ] 결과 저장 +- [ ] 초기화 (GPU 정리 또는 노드 재부팅) + +### 시나리오 2: tini만 +- [ ] 이미지 빌드 & 푸시 +- [ ] Pod 배포 & 진입 +- [ ] 좀비 수거 확인 (tini 효과) +- [ ] gpu-pod-trace.sh로 상태 확인 +- [ ] kill -9 후 orphaned context 확인 +- [ ] Pod 삭제 후 GPU 상태 기록 +- [ ] 결과 저장 +- [ ] 초기화 + +### 시나리오 3: tini + preStop +- [ ] Pod 배포 (이미지는 시나리오 2 재사용) +- [ ] preStop 동작 확인 (로그) +- [ ] SIGTERM 무시 → SIGKILL fallback 확인 +- [ ] gpu-pod-trace.sh로 상태 확인 +- [ ] Pod 삭제 후 GPU 상태 기록 +- [ ] 결과 저장 +- [ ] 초기화 + +### 시나리오 4: Full Fix +- [ ] 이미지 빌드 & 푸시 +- [ ] Pod 배포 & 진입 +- [ ] SIGTERM → graceful CUDA cleanup 확인 +- [ ] 좀비 없음, orphaned context 없음 확인 +- [ ] fuser로도 깨끗한지 확인 +- [ ] gpu-pod-trace.sh → GPU 관련 프로세스 0개 확인 +- [ ] Pod 삭제 후 GPU 메모리 완전 해제 확인 +- [ ] 결과 저장 + +### 정리 +- [ ] 최종 비교표 작성 (`results/comparison.md`) +- [ ] namespace 삭제 (`kubectl delete ns gpu-zombie-test`) +- [ ] 컨테이너 이미지 정리 +- [ ] gpu-4 노드 최종 GPU 상태 확인 diff --git a/results/.gitkeep b/results/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scenario-1-nofix/Dockerfile b/scenario-1-nofix/Dockerfile new file mode 100644 index 0000000..baf51f9 --- /dev/null +++ b/scenario-1-nofix/Dockerfile @@ -0,0 +1,14 @@ +FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 + +RUN apt-get update && apt-get install -y python3 python3-pip procps psmisc && \ + pip3 install torch --break-system-packages && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY parent.py gpu_worker.py ./ +COPY observe.sh /usr/local/bin/observe.sh +RUN chmod +x /usr/local/bin/observe.sh + +# PID 1 = bash (좀비 수거 안 함) +ENTRYPOINT ["/bin/bash", "-c"] +CMD ["sleep infinity"] diff --git a/scenario-1-nofix/gpu_worker.py b/scenario-1-nofix/gpu_worker.py new file mode 100644 index 0000000..25b1568 --- /dev/null +++ b/scenario-1-nofix/gpu_worker.py @@ -0,0 +1,24 @@ +import torch +import time +import os +import signal + +# SIGTERM 무시 — 잘못 작성된 코드 시뮬레이션 +signal.signal(signal.SIGTERM, signal.SIG_IGN) + +print(f"[Worker] PID: {os.getpid()}, PPID: {os.getppid()}") + +# GPU 메모리 할당 (~2GB) +device = torch.device('cuda:0') +tensors = [] +for i in range(8): + t = torch.randn(1024, 1024, 64, device=device) + tensors.append(t) + +allocated = torch.cuda.memory_allocated() / 1024**3 +print(f"[Worker] GPU 메모리 할당 완료: {allocated:.2f} GB") + +# 무한 대기 +while True: + time.sleep(5) + print(f"[Worker] alive, PID={os.getpid()}, PPID={os.getppid()}") diff --git a/scenario-1-nofix/parent.py b/scenario-1-nofix/parent.py new file mode 100644 index 0000000..2c42872 --- /dev/null +++ b/scenario-1-nofix/parent.py @@ -0,0 +1,14 @@ +import subprocess +import time +import os + +print(f"[Parent] PID: {os.getpid()}, PPID: {os.getppid()}") + +# 자식 프로세스 spawn (GPU를 점유할 worker) +child = subprocess.Popen(["python3", "gpu_worker.py"]) +print(f"[Parent] Child spawned, PID: {child.pid}") + +# 부모가 wait() 없이 종료 → 자식은 고아가 됨 +time.sleep(5) +print("[Parent] wait() 없이 종료합니다") +os._exit(0) diff --git a/scenario-1-nofix/pod.yaml b/scenario-1-nofix/pod.yaml new file mode 100644 index 0000000..b64e5df --- /dev/null +++ b/scenario-1-nofix/pod.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-nofix + namespace: gpu-zombie-test + labels: + app: gpu-zombie-test + scenario: nofix +spec: + nodeName: gpu-4 + terminationGracePeriodSeconds: 30 + restartPolicy: Never + imagePullSecrets: + - name: harbor-pull-secret + containers: + - name: worker + image: harbor.cone-chain.net/aipf/gpu-zombie-test:nofix + imagePullPolicy: Always + resources: + limits: + nvidia.com/gpu: 1 diff --git a/scenario-2-tini-only/Dockerfile b/scenario-2-tini-only/Dockerfile new file mode 100644 index 0000000..059648a --- /dev/null +++ b/scenario-2-tini-only/Dockerfile @@ -0,0 +1,18 @@ +FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 + +# 이 이미지(:tini)는 시나리오 3에서도 재사용되므로 prestop.sh를 미리 포함. +# 시나리오 2는 preStop hook 자체가 없어서 prestop.sh를 호출하지 않음. + +RUN apt-get update && apt-get install -y python3 python3-pip procps psmisc tini && \ + pip3 install torch --break-system-packages && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY parent.py gpu_worker.py ./ +COPY observe.sh /usr/local/bin/observe.sh +COPY prestop.sh /usr/local/bin/prestop.sh +RUN chmod +x /usr/local/bin/observe.sh /usr/local/bin/prestop.sh + +# PID 1 = tini (좀비 수거 O, signal 전달 O) +ENTRYPOINT ["tini", "--"] +CMD ["sleep", "infinity"] diff --git a/scenario-2-tini-only/gpu_worker.py b/scenario-2-tini-only/gpu_worker.py new file mode 100644 index 0000000..4b39228 --- /dev/null +++ b/scenario-2-tini-only/gpu_worker.py @@ -0,0 +1,22 @@ +import torch +import time +import os +import signal + +# SIGTERM 무시 — 잘못 작성된 코드 시뮬레이션 +signal.signal(signal.SIGTERM, signal.SIG_IGN) + +print(f"[Worker] PID: {os.getpid()}, PPID: {os.getppid()}") + +device = torch.device('cuda:0') +tensors = [] +for i in range(8): + t = torch.randn(1024, 1024, 64, device=device) + tensors.append(t) + +allocated = torch.cuda.memory_allocated() / 1024**3 +print(f"[Worker] GPU 메모리 할당 완료: {allocated:.2f} GB") + +while True: + time.sleep(5) + print(f"[Worker] alive, PID={os.getpid()}, PPID={os.getppid()}") diff --git a/scenario-2-tini-only/parent.py b/scenario-2-tini-only/parent.py new file mode 100644 index 0000000..0fca469 --- /dev/null +++ b/scenario-2-tini-only/parent.py @@ -0,0 +1,12 @@ +import subprocess +import time +import os + +print(f"[Parent] PID: {os.getpid()}, PPID: {os.getppid()}") + +child = subprocess.Popen(["python3", "gpu_worker.py"]) +print(f"[Parent] Child spawned, PID: {child.pid}") + +time.sleep(5) +print("[Parent] wait() 없이 종료합니다") +os._exit(0) diff --git a/scenario-2-tini-only/pod.yaml b/scenario-2-tini-only/pod.yaml new file mode 100644 index 0000000..33fd753 --- /dev/null +++ b/scenario-2-tini-only/pod.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-tini + namespace: gpu-zombie-test + labels: + app: gpu-zombie-test + scenario: tini +spec: + nodeName: gpu-4 + terminationGracePeriodSeconds: 30 + restartPolicy: Never + imagePullSecrets: + - name: harbor-pull-secret + containers: + - name: worker + image: harbor.cone-chain.net/aipf/gpu-zombie-test:tini + imagePullPolicy: Always + resources: + limits: + nvidia.com/gpu: 1 diff --git a/scenario-3-tini-prestop/pod.yaml b/scenario-3-tini-prestop/pod.yaml new file mode 100644 index 0000000..844b6c6 --- /dev/null +++ b/scenario-3-tini-prestop/pod.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-tini-prestop + namespace: gpu-zombie-test + labels: + app: gpu-zombie-test + scenario: tini-prestop +spec: + nodeName: gpu-4 + terminationGracePeriodSeconds: 60 + restartPolicy: Never + imagePullSecrets: + - name: harbor-pull-secret + containers: + - name: worker + # 시나리오 2와 동일 이미지 — CI는 이 태그를 별도로 빌드하지 않음 + image: harbor.cone-chain.net/aipf/gpu-zombie-test:tini + imagePullPolicy: Always + resources: + limits: + nvidia.com/gpu: 1 + lifecycle: + preStop: + exec: + # 공용 prestop.sh (이미지에 /usr/local/bin/prestop.sh로 포함됨) + # 동작: SIGTERM → 20초 대기 → SIGKILL fallback + # 시나리오 3은 worker가 SIGTERM 무시 → fallback 경로 트리거 + command: ["/bin/sh", "-c", "/usr/local/bin/prestop.sh"] diff --git a/scenario-4-fullfix/Dockerfile b/scenario-4-fullfix/Dockerfile new file mode 100644 index 0000000..ad204c4 --- /dev/null +++ b/scenario-4-fullfix/Dockerfile @@ -0,0 +1,14 @@ +FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 + +RUN apt-get update && apt-get install -y python3 python3-pip procps psmisc tini && \ + pip3 install torch --break-system-packages && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY parent.py gpu_worker_fixed.py ./ +COPY observe.sh /usr/local/bin/observe.sh +COPY prestop.sh /usr/local/bin/prestop.sh +RUN chmod +x /usr/local/bin/observe.sh /usr/local/bin/prestop.sh + +ENTRYPOINT ["tini", "--"] +CMD ["sleep", "infinity"] diff --git a/scenario-4-fullfix/gpu_worker_fixed.py b/scenario-4-fullfix/gpu_worker_fixed.py new file mode 100644 index 0000000..aa3cd13 --- /dev/null +++ b/scenario-4-fullfix/gpu_worker_fixed.py @@ -0,0 +1,44 @@ +import torch +import time +import os +import signal +import sys + +print(f"[Worker] PID: {os.getpid()}, PPID: {os.getppid()}") + +# GPU 메모리 할당 (~2GB) +device = torch.device('cuda:0') +tensors = [] +for i in range(8): + t = torch.randn(1024, 1024, 64, device=device) + tensors.append(t) + +allocated = torch.cuda.memory_allocated() / 1024**3 +print(f"[Worker] GPU 메모리 할당 완료: {allocated:.2f} GB") + +# === 핵심: SIGTERM handler 등록 === +def graceful_shutdown(signum, frame): + print(f"[Worker] Signal {signum} 수신, CUDA cleanup 시작...") + + # 1. tensor 참조 해제 + tensors.clear() + + # 2. GPU 캐시 비우기 + torch.cuda.empty_cache() + + # 3. 동기화 후 종료 + torch.cuda.synchronize() + + after = torch.cuda.memory_allocated() / 1024**3 + print(f"[Worker] cleanup 완료, 잔여 GPU 메모리: {after:.2f} GB") + + sys.exit(0) + +signal.signal(signal.SIGTERM, graceful_shutdown) +signal.signal(signal.SIGINT, graceful_shutdown) + +# 무한 대기 +while True: + time.sleep(5) + print(f"[Worker] alive, PID={os.getpid()}, PPID={os.getppid()}, " + f"GPU={torch.cuda.memory_allocated()/1024**3:.2f}GB") diff --git a/scenario-4-fullfix/parent.py b/scenario-4-fullfix/parent.py new file mode 100644 index 0000000..894ce2a --- /dev/null +++ b/scenario-4-fullfix/parent.py @@ -0,0 +1,12 @@ +import subprocess +import time +import os + +print(f"[Parent] PID: {os.getpid()}, PPID: {os.getppid()}") + +child = subprocess.Popen(["python3", "gpu_worker_fixed.py"]) +print(f"[Parent] Child spawned, PID: {child.pid}") + +time.sleep(5) +print("[Parent] wait() 없이 종료합니다") +os._exit(0) diff --git a/scenario-4-fullfix/pod.yaml b/scenario-4-fullfix/pod.yaml new file mode 100644 index 0000000..aa52119 --- /dev/null +++ b/scenario-4-fullfix/pod.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Pod +metadata: + name: gpu-zombie-fullfix + namespace: gpu-zombie-test + labels: + app: gpu-zombie-test + scenario: fullfix +spec: + nodeName: gpu-4 + terminationGracePeriodSeconds: 90 + restartPolicy: Never + imagePullSecrets: + - name: harbor-pull-secret + containers: + - name: worker + image: harbor.cone-chain.net/aipf/gpu-zombie-test:fullfix + imagePullPolicy: Always + resources: + limits: + nvidia.com/gpu: 1 + lifecycle: + preStop: + exec: + # 공용 prestop.sh — 시나리오 3와 동일 스크립트 + # worker가 SIGTERM graceful 처리하므로 fallback 경로 미트리거 + command: ["/bin/sh", "-c", "/usr/local/bin/prestop.sh"] diff --git a/scripts/gpu-pod-trace.sh b/scripts/gpu-pod-trace.sh new file mode 100755 index 0000000..de34342 --- /dev/null +++ b/scripts/gpu-pod-trace.sh @@ -0,0 +1,188 @@ +#!/bin/bash +# GPU Process → Container → Pod 역추적 스크립트 +# 환경: cgroup v1, containerd, systemd slice 형식 +# gpu-4 노드에서 root로 실행 + +echo "============================================" +echo "GPU Process → Pod 매핑 (cgroup v1)" +echo "시각: $(date)" +echo "노드: $(hostname)" +echo "============================================" + +# --- 함수 정의 --- + +extract_container_id() { + local pid=$1 + # cgroup v1 systemd slice: cri-containerd-<64hex>.scope + grep ':memory:' /proc/$pid/cgroup 2>/dev/null | grep -oP 'cri-containerd-\K[a-f0-9]{64}' +} + +extract_pod_uid() { + local pid=$1 + # kubepods--pod.slice → 하이픈 변환 + local raw=$(grep ':memory:' /proc/$pid/cgroup 2>/dev/null | grep -oP 'pod\K[a-f0-9_]+(?=\.slice)') + if [ -n "$raw" ]; then + echo "$raw" | tr '_' '-' + fi +} + +extract_qos() { + local pid=$1 + local qos=$(grep ':memory:' /proc/$pid/cgroup 2>/dev/null | grep -oP 'kubepods-\K(besteffort|burstable)(?=[\.-])' | head -1) + if [ -n "$qos" ]; then + echo "$qos" + else + grep ':memory:' /proc/$pid/cgroup 2>/dev/null | grep -q 'kubepods' && echo "guaranteed" || echo "host" + fi +} + +get_pod_info() { + local container_id=$1 + local info=$(crictl inspect "$container_id" 2>/dev/null) + [ -z "$info" ] && return + + local pod=$(echo "$info" | grep -oP '"io.kubernetes.pod.name":\s*"\K[^"]+' | head -1) + local ns=$(echo "$info" | grep -oP '"io.kubernetes.pod.namespace":\s*"\K[^"]+' | head -1) + local cont=$(echo "$info" | grep -oP '"io.kubernetes.container.name":\s*"\K[^"]+' | head -1) + echo "${pod}|${ns}|${cont}" +} + +# --- 1단계: GPU 점유 PID 수집 --- + +echo "" +echo "[1] GPU 점유 프로세스 수집 (nvidia-smi + fuser)" + +NVIDIA_PIDS=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null | sort -u) + +FUSER_PIDS="" +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm /dev/nvidiactl; do + [ -e "$dev" ] || continue + PIDS=$(fuser "$dev" 2>/dev/null) + FUSER_PIDS="$FUSER_PIDS $PIDS" +done +FUSER_PIDS=$(echo $FUSER_PIDS | tr ' ' '\n' | sort -u | grep -v '^$') + +ALL_PIDS=$(echo -e "${NVIDIA_PIDS}\n${FUSER_PIDS}" | sort -u | grep -v '^$') + +if [ -z "$ALL_PIDS" ]; then + echo " GPU 점유 프로세스 없음" + echo "" + echo "[!] Ghost Context 체크:" + nvidia-smi --query-gpu=index,memory.used --format=csv,noheader,nounits 2>/dev/null | while IFS=', ' read -r idx mem; do + if [ "$mem" -gt 100 ]; then + echo " ⚠️ GPU $idx: 프로세스 없는데 ${mem}MiB 점유 → GHOST CONTEXT" + echo " 추적 불가. nvidia-smi --gpu-reset -i $idx 또는 노드 재부팅 필요" + else + echo " ✅ GPU $idx: ${mem}MiB (정상)" + fi + done + exit 0 +fi + +echo " nvidia-smi PIDs: $(echo $NVIDIA_PIDS | tr '\n' ' ')" +echo " fuser PIDs: $(echo $FUSER_PIDS | tr '\n' ' ')" + +# --- 2단계: PID → Container → Pod 매핑 --- + +echo "" +echo "[2] PID → Container → Pod 매핑" +echo "--------------------------------------------------------------------------------------------------------------" +printf "%-8s %-6s %-12s %-8s %-12s %-30s %-20s %-15s\n" \ + "PID" "STATE" "GPU_MEM" "NSMI" "QOS" "POD" "NAMESPACE" "CONTAINER" +echo "--------------------------------------------------------------------------------------------------------------" + +for pid in $ALL_PIDS; do + + IN_NSMI="N" + echo "$NVIDIA_PIDS" | grep -q "^${pid}$" && IN_NSMI="Y" + + # 프로세스 생존 + if [ ! -d "/proc/$pid" ]; then + printf "%-8s %-6s %-12s %-8s %-12s %-30s\n" \ + "$pid" "DEAD" "orphaned" "$IN_NSMI" "-" "❌ /proc 없음 — Pod 추적 불가" + continue + fi + + STATE=$(cat /proc/$pid/status 2>/dev/null | grep "^State:" | awk '{print $2}') + + GPU_MEM=$(nvidia-smi --query-compute-apps=pid,used_gpu_memory \ + --format=csv,noheader,nounits 2>/dev/null | grep "^${pid}," | awk -F', ' '{print $2}' | xargs) + [ -z "$GPU_MEM" ] && GPU_MEM="fd_only" + + CONTAINER_ID=$(extract_container_id $pid) + QOS=$(extract_qos $pid) + + if [ -z "$CONTAINER_ID" ]; then + CMD=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | head -c 40) + printf "%-8s %-6s %-12s %-8s %-12s %-30s\n" \ + "$pid" "$STATE" "${GPU_MEM}" "$IN_NSMI" "$QOS" "호스트: $CMD" + continue + fi + + POD_INFO=$(get_pod_info "$CONTAINER_ID") + + if [ -n "$POD_INFO" ]; then + POD_NAME=$(echo "$POD_INFO" | cut -d'|' -f1) + NAMESPACE=$(echo "$POD_INFO" | cut -d'|' -f2) + CONT_NAME=$(echo "$POD_INFO" | cut -d'|' -f3) + printf "%-8s %-6s %-12s %-8s %-12s %-30s %-20s %-15s\n" \ + "$pid" "$STATE" "${GPU_MEM}" "$IN_NSMI" "$QOS" "$POD_NAME" "$NAMESPACE" "$CONT_NAME" + else + SHORT_ID=$(echo "$CONTAINER_ID" | head -c 12) + POD_UID=$(extract_pod_uid $pid) + printf "%-8s %-6s %-12s %-8s %-12s %-30s\n" \ + "$pid" "$STATE" "${GPU_MEM}" "$IN_NSMI" "$QOS" "⚠️ 삭제됨 (ctr:${SHORT_ID} pod:${POD_UID})" + fi +done + +echo "--------------------------------------------------------------------------------------------------------------" + +# --- 3단계: 숨겨진 프로세스 --- + +HIDDEN_PIDS=$(comm -13 <(echo "$NVIDIA_PIDS" | sort) <(echo "$FUSER_PIDS" | sort) 2>/dev/null | grep -v '^$') +if [ -n "$HIDDEN_PIDS" ]; then + echo "" + echo "[3] ⚠️ nvidia-smi 미등록 but GPU fd 보유 프로세스:" + for pid in $HIDDEN_PIDS; do + if [ -d "/proc/$pid" ]; then + CMD=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | head -c 60) + FDS=$(ls -la /proc/$pid/fd 2>/dev/null | grep nvidia | wc -l) + CONTAINER_ID=$(extract_container_id $pid) + if [ -n "$CONTAINER_ID" ]; then + POD_INFO=$(get_pod_info "$CONTAINER_ID") + POD_NAME=$(echo "$POD_INFO" | cut -d'|' -f1) + echo " PID $pid: nvidia fd ${FDS}개, Pod: ${POD_NAME:-unknown}, CMD: $CMD" + else + echo " PID $pid: nvidia fd ${FDS}개, 호스트 프로세스, CMD: $CMD" + fi + fi + done +fi + +# --- 4단계: 요약 --- + +echo "" +echo "[4] 요약" +TOTAL=$(echo "$ALL_PIDS" | grep -v '^$' | wc -l) +DEAD=0; ZOMBIE=0; HIDDEN_COUNT=0 +for pid in $ALL_PIDS; do + [ ! -d "/proc/$pid" ] && DEAD=$((DEAD+1)) + [ -d "/proc/$pid" ] && grep -q "^State:.*Z" /proc/$pid/status 2>/dev/null && ZOMBIE=$((ZOMBIE+1)) +done +[ -n "$HIDDEN_PIDS" ] && HIDDEN_COUNT=$(echo "$HIDDEN_PIDS" | grep -v '^$' | wc -l) + +echo " 전체 GPU 관련 프로세스: $TOTAL" +echo " 정상: $((TOTAL - DEAD - ZOMBIE))" +echo " 죽은 프로세스 (orphaned ctx): $DEAD" +echo " 좀비 프로세스: $ZOMBIE" +echo " nvidia-smi 미표시: $HIDDEN_COUNT" + +if [ "$DEAD" -gt 0 ] || [ "$ZOMBIE" -gt 0 ]; then + echo "" + echo " ⚠️ 조치 필요:" + [ "$ZOMBIE" -gt 0 ] && echo " 좀비 → 부모 프로세스 kill 또는 Pod 재시작" + [ "$DEAD" -gt 0 ] && echo " orphaned ctx → Pod 재시작, 안 되면 nvidia-smi --gpu-reset 또는 노드 재부팅" +fi + +echo "" +echo "============================================" diff --git a/scripts/node-observe.sh b/scripts/node-observe.sh new file mode 100755 index 0000000..03a9eb4 --- /dev/null +++ b/scripts/node-observe.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# gpu-4 노드에서 root로 실행 — 노드 레벨 GPU 관찰 +echo "==========================================" +echo "노드 레벨 GPU 관찰: $(date)" +echo "==========================================" +echo "" +echo "[1] nvidia-smi 전체" +nvidia-smi +echo "" +echo "[2] GPU 프로세스 목록" +nvidia-smi --query-compute-apps=pid,used_gpu_memory,name --format=csv +echo "" +echo "[3] /dev/nvidia* 사용 프로세스 (fuser)" +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm /dev/nvidiactl; do + [ -e "$dev" ] || continue + PIDS=$(fuser "$dev" 2>/dev/null) + if [ -n "$PIDS" ]; then + echo " $dev →$PIDS" + fi +done +echo "" +echo "[4] nvidia-smi 미등록 but /dev/nvidia fd 보유 프로세스" +NVIDIA_PIDS=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null | sort -u) +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm /dev/nvidiactl; do + [ -e "$dev" ] || continue + for pid in $(fuser "$dev" 2>/dev/null); do + if ! echo "$NVIDIA_PIDS" | grep -q "^${pid}$"; then + CMD=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | head -c 60) + echo " ⚠️ PID $pid ($dev): nvidia-smi 미등록, CMD: $CMD" + fi + done +done +echo "" +echo "[5] Ghost context 체크 (프로세스 0 but 메모리 점유)" +PROC_COUNT=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null | grep -v '^$' | wc -l) +nvidia-smi --query-gpu=index,memory.used --format=csv,noheader,nounits 2>/dev/null | while IFS=', ' read -r idx mem; do + if [ "$PROC_COUNT" -eq 0 ] && [ "$mem" -gt 100 ]; then + echo " ⚠️ GPU $idx: 프로세스 0개인데 ${mem}MiB 점유 → GHOST CONTEXT" + fi +done +echo "==========================================" diff --git a/scripts/observe.sh b/scripts/observe.sh new file mode 100755 index 0000000..229d0bd --- /dev/null +++ b/scripts/observe.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# 컨테이너 내부 관찰 스크립트 — 좀비/고아/GPU 상태 종합 점검 +echo "==========================================" +echo "컨테이너 내부 관찰: $(date)" +echo "==========================================" + +echo "" +echo "[1] PID 1 확인" +cat /proc/1/cmdline | tr '\0' ' ' +echo "" + +echo "" +echo "[2] 전체 프로세스 트리" +ps auxf +echo "" + +echo "" +echo "[3] 좀비 프로세스 (STAT=Z)" +ZOMBIES=$(ps aux | awk '$8 ~ /Z/') +if [ -z "$ZOMBIES" ]; then + echo " → 좀비 없음 ✅" +else + echo " → 좀비 발견 ❌" + echo "$ZOMBIES" +fi +echo "" + +echo "" +echo "[4] 고아 프로세스 (PPID=1, PID 1 자체 제외)" +ORPHANS=$(ps -eo pid,ppid,stat,cmd | awk '$2==1 && $1!=1') +if [ -z "$ORPHANS" ]; then + echo " → 고아 없음 ✅" +else + echo " → 고아 발견 ⚠️" + echo "$ORPHANS" +fi +echo "" + +echo "" +echo "[5] GPU 메모리 상태" +nvidia-smi --query-gpu=index,memory.used,memory.total --format=csv +echo "" + +echo "" +echo "[6] GPU 사용 프로세스 (nvidia-smi)" +GPU_PIDS=$(nvidia-smi --query-compute-apps=pid,used_gpu_memory,name --format=csv,noheader 2>/dev/null) +if [ -z "$GPU_PIDS" ]; then + echo " → nvidia-smi에 프로세스 없음" +else + echo "$GPU_PIDS" +fi +echo "" + +echo "" +echo "[7] /dev/nvidia* fd 보유 프로세스 (nvidia-smi에 안 나올 수 있음)" +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm /dev/nvidiactl; do + [ -e "$dev" ] || continue + PIDS=$(fuser "$dev" 2>/dev/null) + if [ -n "$PIDS" ]; then + echo " $dev →$PIDS" + fi +done +echo "" + +echo "" +echo "[8] Orphaned GPU Context 종합 체크" + +# 8-a. nvidia-smi에 있는 PID 중 죽은 것 +echo " 8-a. nvidia-smi 등록 프로세스 생존 확인:" +NSMI_PIDS=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null) +if [ -z "$NSMI_PIDS" ]; then + echo " nvidia-smi에 등록된 프로세스 없음" +else + for pid in $NSMI_PIDS; do + if [ -d "/proc/$pid" ]; then + STATE=$(cat /proc/$pid/status 2>/dev/null | grep "^State:" | awk '{print $2}') + echo " PID $pid: 살아있음 (State=$STATE)" + else + echo " PID $pid: 죽었음 → ❌ ORPHANED GPU CONTEXT" + fi + done +fi + +# 8-b. nvidia-smi에 안 나오지만 fuser에 잡히는 프로세스 +echo " 8-b. nvidia-smi 미등록 but GPU fd 보유:" +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm /dev/nvidiactl; do + [ -e "$dev" ] || continue + for pid in $(fuser "$dev" 2>/dev/null); do + if ! echo "$NSMI_PIDS" | grep -q "^${pid}$"; then + if [ -d "/proc/$pid" ]; then + CMD=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | head -c 50) + echo " ⚠️ PID $pid ($dev): $CMD" + fi + fi + done +done + +# 8-c. Ghost context (프로세스 0인데 메모리 점유) +echo " 8-c. Ghost context 체크:" +PROC_COUNT=$(echo "$NSMI_PIDS" | grep -v '^$' | wc -l) +nvidia-smi --query-gpu=index,memory.used --format=csv,noheader,nounits 2>/dev/null | while IFS=', ' read -r idx mem; do + if [ "$PROC_COUNT" -eq 0 ] && [ "$mem" -gt 100 ]; then + echo " ⚠️ GPU $idx: 프로세스 없는데 ${mem}MiB 점유 → GHOST CONTEXT" + else + echo " GPU $idx: ${mem}MiB (프로세스 ${PROC_COUNT}개)" + fi +done + +echo "" +echo "==========================================" diff --git a/scripts/prestop.sh b/scripts/prestop.sh new file mode 100755 index 0000000..b35b312 --- /dev/null +++ b/scripts/prestop.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# /usr/local/bin/prestop.sh +# Pod preStop hook용 GPU cleanup 스크립트 +# 시나리오 3·4 공통 사용 +# - 시나리오 3 (:tini, worker가 SIGTERM 무시): fallback 경로 트리거 +# - 시나리오 4 (:fullfix, worker graceful): fallback 경로 미트리거 + +echo "[preStop] GPU cleanup 시작: $(date)" + +# 1. nvidia-smi + fuser로 GPU 점유 PID 수집 +GPU_PIDS=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null) +FUSER_PIDS="" +for dev in /dev/nvidia[0-9]* /dev/nvidia-uvm; do + [ -e "$dev" ] || continue + PIDS=$(fuser "$dev" 2>/dev/null) + FUSER_PIDS="$FUSER_PIDS $PIDS" +done +ALL_PIDS=$(echo "$GPU_PIDS $FUSER_PIDS" | tr ' ' '\n' | sort -u | grep -v '^$') +echo "[preStop] 전체 GPU 관련 PIDs: $ALL_PIDS" + +# 2. PID 1(tini) 제외하고 SIGTERM 전송 +for pid in $ALL_PIDS; do + if [ "$pid" != "1" ]; then + echo "[preStop] SIGTERM -> PID $pid" + kill -TERM $pid 2>/dev/null + fi +done + +# 3. 최대 20초 대기 — graceful cleanup +for i in $(seq 1 20); do + REMAINING=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null | wc -l) + if [ "$REMAINING" -eq 0 ]; then + echo "[preStop] GPU 프로세스 정리 완료 ✅" + break + fi + echo "[preStop] 대기 ($i/20) 남은 프로세스: $REMAINING" + sleep 1 +done + +# 4. SIGKILL fallback (시나리오 3에서 트리거됨) +STILL=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null | wc -l) +if [ "$STILL" -gt 0 ]; then + echo "[preStop] ⚠️ SIGTERM 무시됨 → SIGKILL fallback" + for pid in $(nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null); do + kill -9 $pid 2>/dev/null + done +fi + +echo "[preStop] 완료: $(date)"