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