1518 lines
47 KiB
Markdown
1518 lines
47 KiB
Markdown
# GPU Zombie/Orphan Process & Orphaned CUDA Context 재현 및 해결 실험 계획서
|
|
|
|
## 실험 환경
|
|
|
|
| 항목 | 값 |
|
|
|---|---|
|
|
| 노드 | `gpu-4` (A100) |
|
|
| 재부팅 가능 여부 | O |
|
|
| 컨테이너 런타임 | containerd |
|
|
| cgroup | **v1 (systemd slice 형식)** |
|
|
| 실험 도구 | Claude CLI (claude code) |
|
|
|
|
### cgroup v1 경로 형식 (확인 완료)
|
|
|
|
```
|
|
# /proc/<PID>/cgroup 출력 형태:
|
|
9:memory:/kubepods.slice/kubepods-<QOS>.slice/kubepods-<QOS>-pod<POD_UID>.slice/cri-containerd-<CONTAINER_ID>.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-<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 "============================================"
|
|
```
|
|
|
|
---
|
|
|
|
## 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:<tag>` 는 모두 `harbor.cone-chain.net/aipf/gpu-zombie-test:<tag>` 로 매핑된다.**
|
|
> 이미지 태그: `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 = <Harbor CLI Secret> # .env의 REGISTRY_PASSWORD
|
|
# ARGOCD_URL = https://argocd.gpulive.cloud
|
|
# ARGOCD_USER = admin
|
|
# ARGOCD_PASSWORD = <ArgoCD admin 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='<Harbor CLI Secret>' \
|
|
-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 <worker_pid>` 실행
|
|
- [ ] `ls /proc/<worker_pid>` → "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_pid>`
|
|
- [ ] worker가 SIGTERM 무시하는지 확인 (프로세스 살아있음)
|
|
- [ ] `kill -9 <worker_pid>` 후 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 <worker_pid>
|
|
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/<any_container_pid>/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 상태 확인
|