Initial commit — GPU zombie/orphan/orphaned-CUDA-context 실험 프로젝트
- 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) <noreply@anthropic.com>
This commit is contained in:
commit
1daa9bad24
|
|
@ -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
|
||||||
|
|
@ -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 }}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
results/*/
|
||||||
|
!results/.gitkeep
|
||||||
|
.claude/
|
||||||
|
|
@ -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-<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 스택 사용 중
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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 }}
|
||||||
|
|
@ -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 }}
|
||||||
|
|
@ -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 }}
|
||||||
|
|
@ -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 }}
|
||||||
|
|
@ -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 }}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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()}")
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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()}")
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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-<qos>-pod<UUID_underscores>.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 "============================================"
|
||||||
|
|
@ -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 "=========================================="
|
||||||
|
|
@ -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 "=========================================="
|
||||||
|
|
@ -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)"
|
||||||
Loading…
Reference in New Issue