[GPU 모델 병렬 처리] Single GPU Inference Using GPU to the Max Potential 요약 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

원문 : https://vasusharma7.medium.com/single-gpu-inference-using-gpu-to-the-max-potential-8b2aebf5ca7b


1. 문제 상황

  • 저자는 FastAPI + PyTorch 기반으로 이미지 인퍼런스를 처리하는 API 서버 운영
  • 클라이언트로부터 비동기 요청이 여러 개 동시에 들어오지만, GPU 사용률이 낮고 응답 시간 개선이 없음
  • 일반적인 async def 핸들러로도 GPU 추론은 직렬처럼 수행됨.
  • 원인 추정: Python GIL 및 PyTorch의 단일 스레드 GPU 실행 환경에서 비동기 요청이 병렬로 처리되지 않음.

2. 실패한 시도들

a. CUDA Streams

s = torch.cuda.Stream()
with torch.cuda.stream(s):
    outputs = model(inputs)
  • 여러 개의 CUDA stream을 생성하여 각각의 요청을 분리된 stream에서 실행.
  • 기대: GPU 커널을 stream 별로 병렬 실행하여 자원 활용 극대화.
  • 결과: 효과 없음.
    • 모델이 작은 편이라 각 stream이 처리하는 연산량이 작고, 커널 실행 자체가 빠름.
    • stream 간 오버랩이 발생하기도 전에 연산 종료됨.

b. Multiprocessing

  • 요청당 별도의 프로세스를 할당하여 GPU 작업 실행 시도.
  • 목적: GIL 회피 및 GPU 연산 병렬화.
  • 결과: 실패.
    • 구체적 이유는 명시되지 않았지만, PyTorch 모델이 메모리를 공유하지 못하거나 CUDA context 문제 등으로 병렬성이 확보되지 않음.

c. Python 3.13 Free-threading

  • GIL 제거가 실험적으로 도입된 Python 3.13을 테스트해보고자 함.
  • 결과: 시도하지 못함.
    • PyTorch, FastAPI 등의 핵심 라이브러리가 아직 호환되지 않음.

3. 최종 해결책: 요청을 모아서 배치 처리(동적 배치 처리)

a. 핵심 아이디어

  • 단일 요청을 하나하나 처리하지 않고, 짧은 시간 동안 요청들을 큐에 모아두었다가 한번에 배치 처리.
  • 이 배치를 모델에 통째로 넣어 forward 함으로써 GPU를 한 번에 더 많이 활용.

b. 구현 방식 (예시 코드)

가. FastAPI 엔드포인트

@app.post("/process")
async def process(request: Request):
    body = await request.json()
    uid = uuid.uuid4()
    request_queue.put({"id": uid, "inp": body["input"]})

    while uid not in response:
        await asyncio.sleep(0.01)

    resp = response[uid]
    del response[uid]
    return resp

나. 백그라운드 워커 (추론 실행)

def process_requests():
    while True:
        if request_queue.empty():
            continue
        requests = []
        while not request_queue.empty():
            requests.append(request_queue.get())

        inputs = [r["inp"] for r in requests]
        uids = [r["id"] for r in requests]

        preds = model(inputs)  # 👉 배치 인퍼런스
        for i, uid in enumerate(uids):
            response[uid] = preds[i]

다. 설명

  • request_queue: 요청을 임시 저장하는 큐
  • response: 결과를 각 요청 ID와 매핑하여 반환
  • process_requests: 별도의 스레드에서 동작하며, 큐에 쌓인 요청들을 모아 배치로 처리함

c. 성능 결과

  • 요청 수가 늘어날수록 배치 크기가 자연스럽게 커지며 GPU 사용률 증가.
  • GPU 연산 시간이 조금 늘었지만, 전체 처리량(throughput)은 40% 이상 증가.
  • NVIDIA-SMI 기준 GPU 메모리 및 연산 활용률이 이전보다 눈에 띄게 증가.

4. 실무적인 교훈

  • 단순 비동기 구조만으로는 GPU 병렬화 효과를 얻기 어려움.
  • stream 병렬화도 모델이 작을 경우 효과가 없음.
  • 동적 배치(Dynamic Batching)가 가장 효과적인 GPU 활용 방법.
    • 모델이 크지 않아도, 요청을 묶어서 처리하는 방식으로 자원 활용 극대화 가능.
  • 배치 처리 시 latency 증가 없이 throughput 향상 가능 (적절한 큐 대기 시간 설정 필요).

5. 결론

  • 이 글은 PyTorch 모델을 FastAPI 서버에서 서빙할 때 GPU를 최대한 활용하려는 여러 시도와 그 성과를 공유.
  • 특히, 현실적인 제약(Python GIL, CUDA stream의 한계 등) 속에서 가장 효과적이었던 해결책은 “동적 배치 처리”라는 점을 실험으로 증명
  • 작성자는 이 방식만으로도 단일 GPU의 잠재력을 훨씬 더 끌어낼 수 있었다고 결론