gpu-zombie-test/README.md

20 KiB
Raw Permalink Blame History

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 참조. 5개 변형 실험 결과 비교는 results/comparison.md.


실험 결과 요약 (2026-04-13~14)

시나리오 1을 5가지 변형으로 실행한 결과 plan.md의 핵심 가설("orphan CUDA context 발생")이 현대 driver(580/590)에서 검증되지 않음. 대신 두 가지 다른 진짜 문제가 더 중요한 것으로 드러남.

핵심 발견

가설 (plan.md) 실제 결과 비고
kill -9 후 orphaned CUDA context 잔존 미발생 driver가 즉시 회수
Pod 삭제 후 GPU 메모리 잔존 미발생 cgroup SIGKILL → driver 회수
노드 재부팅 필요 가능 불필요 모든 케이스 자동 정리
좀비/고아 발생 재현 성공 tini로 해결됨 (시나리오 2 효과 확인)
30초 grace 동안 GPU 낭비 발견 (진짜 문제) SIGTERM 무시 시 항상 발생
NCCL hang으로 GPU 점유 발견 (예상 못한 문제) 멀티 GPU 환경에서 발생

진짜 문제 vs plan.md 원래 가설

진짜 문제 발생 조건 영향 시나리오 X에서 해결
30초 grace 낭비 SIGTERM 무시 코드 GB·sec 낭비 (10w일 때 740 GB·sec) 시나리오 4 (SIGTERM handler)
좀비 누적 PID 1 ≠ init 프로세스 슬롯 점유 시나리오 2 (tini)
NCCL hang rank 비정상 종료 무한 GPU 점유 NCCL_ASYNC_ERROR_HANDLING + timeout
Peer ctx 잔존 NCCL 멀티 GPU 작은 누수 (148 MiB) NCCL hang 해결 시 자동

5개 변형 실험

# 변형 노드/Driver 핵심 발견 결과 디렉토리
1 기본 (root, 1 worker) gpu-4 / 590.48.01 orphan ctx 미발생 results/scenario-1/
2 구버전 driver v1003 / 580.126.09 driver 버전 무관 동일 results/scenario-1-driver-580.126.09/
3 non-root (UID 1000) gpu-4 / 590.48.01 sudo 없이 kill 가능 results/scenario-1-user-soo/
4 10 workers 동시 gpu-4 / 590.48.01 선형 스케일, 740 GB·sec 낭비 results/scenario-1-user-soo-10workers/
5 NCCL 3 GPU gpu-4 / 590.48.01 부분 누수 + hang (유일) results/scenario-1-nccl-3gpu/

자세한 비교 분석: results/comparison.md

시나리오 2~4의 재정의된 가치

시나리오 원래 목적 (plan.md) 실제 가치
2 (tini) orphan ctx 방지 좀비 reaping (init protection)
3 (preStop) orphan ctx 방지 GPU cleanup 명령 grace 활용
4 (Full Fix) orphan ctx 방지 30초 grace 낭비 제거 + NCCL graceful shutdown

배경 — 왜 이 실험이 필요한가

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 contextnvidia-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 (실험 완료, 5가지 변형으로 검증)

가설: PID 1이 bash면 좀비/고아/orphaned CUDA context가 모두 발생한다.

검증 결과:

  • 고아 발생 — parent.py 종료 후 gpu_worker.py PPID=1 reparent
  • 좀비 발생 — kill -9 후 PID 1(sleep)이 reaping 안 함
  • orphaned CUDA context 미발생 — driver 590/580 모두 즉시 회수
  • Pod 삭제 후 GPU 잔존 미발생 — cgroup SIGKILL이 항상 정리
  • 새 발견: SIGTERM 무시 시 30초 grace 동안 GPU 점유 지속 (배포/스케일 시 리소스 낭비)
  • 새 발견 (NCCL): rank 0 죽으면 rank 1, 2가 hang → GPU 5.7GB 영구 점유 (Pod 삭제 전까지)

Tip: PID 1 = sleep infinity (bash exec 최적화로 sleep이 PID 1을 대체하지만, init 아닌 동작은 동일)

시나리오 2 — tini only (미실험, 가설 갱신)

가설: tini가 PID 1이면 좀비는 수거되지만, 코드에 signal handler가 없으면 CUDA cleanup은 여전히 실패한다.

원래 가설:

  • tini의 waitpid(-1) 덕분에 좀비는 사라짐 → 예상

갱신된 가치 (시나리오 1 결과 반영):

  • 좀비 reaping — 시나리오 1의 좀비 발생을 막는 init 보호
  • orphan CUDA ctx 방지 가치는 사실상 없음 (시나리오 1에서 driver가 자동 회수)
  • ⚠️ 30초 grace 낭비는 그대로 (signal handler 없으므로 동일)

시나리오 3 — tini + preStop (미실험, 가설 갱신)

가설: preStop hook으로 SIGTERM 미리 보내도 worker가 무시하면 SIGKILL fallback 발생.

갱신된 가치:

  • preStop이 GPU cleanup 명령(/usr/local/bin/prestop.sh)을 grace period 안에 미리 실행
  • 다만 worker가 SIGTERM 무시하면 시나리오 2와 동일하게 30초 후 SIGKILL
  • ⚠️ 본질적으로 application 코드가 SIGTERM을 처리하지 않으면 효과 제한적

시나리오 4 — Full Fix (미실험, 가장 큰 가치 기대)

가설: tini + preStop + 코드 signal handler 모두 적용 시 깨끗한 정리.

갱신된 가치 (가장 중요):

  • 30초 grace 낭비 → 0초로 단축 (시나리오 1에서 발견된 진짜 문제 해결)
  • NCCL dist.destroy_process_group() 호출 — multi-GPU에서 5.7GB 누수 방지
  • worker가 SIGTERM 받아서 즉시 tensors.clear()torch.cuda.empty_cache() → exit
  • 결과적으로 GPU 메모리 빠른 반환 + application graceful shutdown
  • Pod 삭제 후 GPU 메모리 완전 해제, ghost context 없음

최종 비교표 (S1 실측 + S2~4 갱신 예상)

항목 S1 (실측) S2 (예상) S3 (예상) S4 (예상)
좀비 수거 발생 (1~10개) tini가 reap
고아 발생 (PPID=1) but tini가 reap but reap 미발생 (handler가 정리)
SIGTERM 처리 SIG_IGN SIG_IGN but preStop이 cleanup 시도 graceful
kill -9 후 orphaned CUDA ctx 미발생 (driver 회수) 동일 예상 동일 예상 N/A
Pod 삭제 시 GPU 점유 시간 30초 (grace 전체) 30초 (handler 없음) 30초 (worker 무시) 0초
Pod 삭제 후 GPU 메모리 해제 (SIGKILL 후) 해제 해제 즉시 해제
노드 재부팅 필요 불필요
NCCL hang 시 GPU 점유 5.7GB 영구 점유 동일 (handler 없음) preStop도 hang 못 풂 NCCL_ASYNC + destroy_process_group

시나리오 2~4는 미실험. 시나리오 1 결과를 바탕으로 갱신된 예상치. 실측 시 results/ 업데이트.


디렉토리 구조

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 — 시나리오 실행 사이클

각 시나리오는 다음 공통 흐름을 따름:

# 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 생존 확인)

⚠️ 알려진 한계 (시나리오 1 실험에서 발견): torch 사용 시 nvidia-smi가 컨테이너 내부 PID가 아닌 노드 PID를 반환 → observe.sh가 컨테이너 내부 /proc/<노드 PID> 조회 실패 → "ORPHANED" 오탐지 발생. 정확한 orphaned context 판정은 노드 레벨 gpu-pod-trace.sh에서만 가능. raw CUDA driver API(ctypes) 사용 시는 컨테이너 PID가 일치해서 오탐지 없음.

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 & 클러스터 상태

# 배포된 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 상태 (노드에서)

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 환경) 완료 상태.

# 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 — 모든 스크립트·매니페스트·체크리스트 포함 (1500+ 줄)
  • Claude Code 권한: .claude/settings.json — 이 프로젝트 한정 권한
  • CI/CD 참고 템플릿: /home/ubuntu/soo/vLLM/A100_npu/ — 동일 Gitea/Harbor/ArgoCD 스택 사용 중