[트러블 슈팅] GPU 서버 멀티 프로세스 도입 (gunicorn cuda does not allow reinitialize) - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

1. 개요

  • cpu 서버와 동일하게 gpu 서버에도 gunicorn을 이용하여 멀티 프로세스를 도입하려 함
  • 그러나 gpu 서버의 cuda 특성 상 gunicorn을 이용하는 상황에서 ‘gunicorn cuda does not allow reinitialize’ 에러 발생
  • 이 때문에 최종적으로는 uvicorn 기반의 멀티 프로세스로 아키텍쳐 수정
  • 트러블 슈팅 과정 아래에서 서술

2. 트러블 슈팅 흐름

a. 흐름: clip, arcface, yoloface lazy loading으로 수정 → uvicorn 아키텍쳐로 수정

b. clip, arcface, yoloface lazy loading으로 수정

image

가. 에러 상황

  • gpu 서버를 띄우려고 할 때 위와 같은 ‘멀티 프로세스 환경에서 CUDA 재초기화 불가’ 에러 발생

나. 원인: model.to(device) 통해 gpu에 올리려고 하지만 cuda context 없음 → 재초기화 불가

  • 모델을 부모 프로세스에 로딩할 때, cuda context 생성이 일어남
  • fork() 시스템 콜 호출 시, 가상 메모리 테이블 복제되고 메모리 페이지는 읽기 전용으로 공유됨
  • cuda context는 gpu driver 레벨에 존재하기 때문에 fork()의 영향을 받지 않음
    • 자식 프로세스는 cuda context가 없는 상태, 그러나 메모리에 cuda 관련 라이브러리 로딩되어 있어 온전히 cuda context 없는 상태로 판단되지 않음
  • 자식 프로세스 생성되며 model.to(device)를 통해 메모리에 로딩된 모델을 GPU로 올리기 위해 cuda context를 이용하려 하고 cuda context가 없어서 초기화 시도
    • CUDA 런타임 상태의 일부가 남아있어 재초기화로 판단됨 cuda 원칙 상 허용하지 않음

다. 해결: lazy loading으로 변경

  • 모델을 서버 띄울 때, 로딩해서 부모 프로세스에서 cuda context 생성이 일어났다고 판단
  • 이에 실제 첫 번째 요청이 들어왔을 때 로딩하는 lazy loading 도입
  • 이를 통해 자식 프로세스들에서 각각 cuda context 초기화와 cuda 관련 라이브러리 임포트하게 하고자 함

c. Uvicorn 기반 멀티 프로세스 구조로 수정

image 1

가. 에러 상황

  • lazy loading 방식으로 변경한 후, 서버 띄워졌지만 실제 요청 보냈을 때 CUDA 재초기화 불가라는 동일한 에러 발생

나. 원인: CUDA 관련 라이브러리가 부모 프로세스에서 import되어 CUDA 런타임이 일부 존재

  • 모델은 로딩하지 않았음
  • 그러나 부모 프로세스에서 아래와 같은 과정 진행됨:
    • 라이브러리 import를 통해 메모리에 CUDA 런타임 일부가 생성됨
  • 자식 프로세스를 fork() 시스템 콜로 생성할 때, 메모리 공유 일어남
  • 서버 띄울 때, model.to(device)를 하지 않기 때문에 CUDA 런타임이 부분적으로 존재하는 문제가 확인되지 않음
  • 요청 보냈을 때, model.to(device)가 발생하지만 cuda context가 없는 상태로 초기화 시도
  • cuda 원칙 상 CUDA 런타임이 이미 있기 때문에 재초기화라 판단해 에러 발생

다. 해결: uvicorn 기반 멀티 프로세스 구조로 수정

  • uvicorn은 spawn() 방식으로 자식 프로세스 생성
  • spawn() 시스템 콜은 부모 프로세스의 메모리 복제 및 공유 없이 프로세스를 생성함
    • 그렇기에 부모 프로세스의 cuda 라이브러리 및 런타임 로딩과 무관하게 자식 프로세스에서 cuda context 생성 일어남
    • 이를 통해 자식 프로세스에서 안전하게 CUDA 런타임을 확보