AI 모델을 프로덕션 환경에 서빙할 때, 예상치 못한 GPU 메모리 부족(OOM: Out-Of-Memory) 오류는 엔지니어들을 가장 당황스럽게 만드는 문제 중 하나입니다. 특히 모델 자체의 메모리 사용량이 명확하게 계산되더라도, 장시간 운영되거나 다양한 크기의 요청이 불규칙하게 들어올 때 OOM이 발생하여 서비스가 중단되는 경우가 빈번합니다. 이런 상황은 단순히 GPU 메모리를 늘리는 것만으로는 해결되지 않으며, 근본적인 원인인 메모리 단편화를 이해하고 해결해야 합니다.
최근 저희 팀에서도 특정 AI 추론 서비스가 운영 시간 24시간을 넘어가면 간헐적으로 CUDA out of memory. Tried to allocate X GiB (GPU 0; Y GiB total capacity; Z GiB already allocated; W GiB free; A GiB reserved in total by PyTorch)와 같은 에러 메시지를 뿜으며 크래시되는 현상을 겪었습니다. 초기에는 모델의 복잡성 증가나 배치 사이즈 문제로 접근했지만, 동일한 모델과 배치 사이즈에서도 비정기적으로 발생한다는 점에서 메모리 관리 방식에 문제가 있음을 직감했습니다. 이 글에서는 AI 모델 서빙 환경에서 GPU 메모리 단편화로 인한 OOM 문제를 진단하고, 실무에서 적용할 수 있는 해결책들을 깊이 있게 다룹니다.
GPU 메모리 단편화의 메커니즘과 OOM 발생 원인
GPU 메모리 단편화는 운영체제의 메인 메모리 단편화와 유사하게, 메모리 할당 및 해제 작업이 반복되면서 사용 가능한 연속된 메모리 블록이 파편화되는 현상을 의미합니다. AI 모델 추론 과정에서는 다양한 크기의 텐서(Tensor) 객체들이 생성되고 해제됩니다. 예를 들어, 입력 데이터의 크기, 중간 연산 결과, 임시 버퍼 등이 매 요청마다 동적으로 할당되고 해제됩니다.
이러한 동적인 메모리 관리 과정에서, 작은 메모리 블록들이 여기저기 흩어져 존재하게 되고, 정작 큰 텐서를 할당해야 할 때는 전체 GPU 메모리 용량이 충분함에도 불구하고 연속된 충분한 크기의 메모리 블록을 찾지 못해 OOM이 발생하게 됩니다. 마치 넓은 주차장에 빈 공간은 많지만, 연속된 두 칸짜리 빈자리가 없어 대형 트럭이 주차할 수 없는 상황과 같습니다. 특히 PyTorch나 TensorFlow 같은 딥러닝 프레임워크는 자체적인 메모리 캐싱 및 할당 전략을 가지고 있어, OS 레벨의 메모리 관리와는 다른 양상을 보이기도 합니다.
CUDA 메모리 할당 전략 최적화
PyTorch와 TensorFlow는 CUDA 메모리 할당을 위한 자체적인 캐싱 메커니즘을 가지고 있습니다. 이 메커니즘은 작은 메모리 할당 요청에 대해 GPU 드라이버를 직접 호출하는 오버헤드를 줄이고 성능을 향상시키기 위함입니다. 그러나 이 캐싱이 과도해지면 해제된 메모리를 즉시 OS에 반환하지 않고 내부적으로 유지하므로, 단편화가 심화될 수 있습니다.
1. PyTorch 캐시 비우기 및 초기화
PyTorch의 경우, 추론 요청 사이에 torch.cuda.empty_cache()를 호출하여 사용하지 않는 캐시된 메모리 블록을 해제하고 OS에 반환하도록 유도할 수 있습니다. 이는 특히 배치 추론(batch inference)이나 여러 모델을 순차적으로 로드/언로드하는 시나리오에서 효과적입니다.
import torch
def inference_with_cache_clear(model, input_data):
# 모델 추론 수행
with torch.no_grad():
output = model(input_data.to('cuda'))
# 추론 후 CUDA 캐시 비우기
torch.cuda.empty_cache() # 사용되지 않는 GPU 메모리 캐시를 비워 단편화를 줄입니다.
return output
# 예시: 웹 서비스 요청 처리 루프
# while True:
# request_data = get_request()
# result = inference_with_cache_clear(my_model, request_data)
# send_response(result)
2. TensorFlow 메모리 성장(Growth) 설정
TensorFlow 2.x부터는 기본적으로 GPU 메모리를 필요한 만큼만 할당하는 '메모리 성장' 전략이 활성화되어 있습니다. 하지만 이전 버전이나 특정 설정에서는 전체 GPU 메모리를 미리 할당하는 경우가 있습니다. 이를 명시적으로 설정하여 메모리 단편화를 완화할 수 있습니다.
import tensorflow as tf
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
try:
# 모든 GPU에 대해 메모리 성장을 활성화합니다.
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True) # GPU 메모리를 필요한 만큼만 할당하도록 설정하여 단편화를 줄입니다.
logical_gpus = tf.config.experimental.list_logical_devices('GPU')
print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
except RuntimeError as e:
# 프로그램 시작 시 메모리 성장을 설정해야 합니다.
print(e)
컨테이너 환경에서의 리소스 격리 및 관리
AI 모델 서빙은 대부분 Docker나 Kubernetes와 같은 컨테이너 환경에서 이루어집니다. 이 환경에서는 컨테이너별 GPU 리소스 격리 및 관리가 중요합니다. 단일 GPU를 여러 컨테이너가 공유하거나, 컨테이너 내에서 메모리 제한이 제대로 설정되지 않으면 OOM 문제가 더욱 악화될 수 있습니다.
1. NVIDIA Container Toolkit (nvidia-docker) 활용
nvidia-docker를 사용하여 컨테이너에 GPU를 할당하고, docker run 명령 시 --gpus 옵션으로 특정 GPU를 지정하거나 메모리 제한을 설정할 수 있습니다.
# 특정 GPU (예: GPU 0)를 컨테이너에 할당하고 메모리 사용량을 제한합니다.
docker run --gpus '"device=0"' -m 8g --memory-swap -1 your_ai_service_image:latest # GPU 0번을 사용하고, 컨테이너 메모리를 8GB로 제한하며, 스왑을 비활성화합니다.
# 또는 모든 GPU를 할당하되, 각 컨테이너가 사용하는 GPU 메모리 양을 제한합니다.
# 이 방법은 GPU 메모리 단편화를 직접적으로 해결하기보다,
# 각 컨테이너가 과도하게 메모리를 점유하여 다른 컨테이너에 영향을 주는 것을 방지합니다.
# 실제로 CUDA 레벨의 메모리 제한은 프레임워크 설정으로 더 정교하게 제어됩니다.
# docker run --gpus all --memory 8g --memory-swap -1 your_ai_service_image:latest
여기서 -m 8g는 컨테이너의 호스트 메모리(RAM) 제한이며, --gpus 옵션은 GPU 디바이스 자체를 할당합니다. GPU 메모리 사용량에 대한 직접적인 제한은 --gpus 옵션 내에서 memory=X 형태로 지원될 수 있으나, 일반적으로는 CUDA 환경 변수를 통해 더 세밀하게 제어됩니다.
2. CUDA 환경 변수를 통한 GPU 메모리 제한
컨테이너 내부에서 CUDA 환경 변수를 설정하여 GPU 메모리 사용량을 제한할 수 있습니다. 이는 특히 PyTorch나 TensorFlow가 전체 GPU 메모리를 미리 할당하는 것을 방지하는 데 유용합니다.
# Dockerfile 예시:
FROM nvidia/cuda:11.8.0-base-ubuntu22.04
# ... (기타 설치 및 설정) ...
ENV NVIDIA_VISIBLE_DEVICES=all
ENV NVIDIA_DRIVER_CAPABILITIES=all
# PyTorch의 경우, CUDA_VISIBLE_DEVICES를 통해 사용할 GPU를 지정하고,
# 모델 로드 전 메모리 할당 비율을 제한할 수 있습니다.
# 예를 들어, GPU 0번의 80%만 사용하도록 설정 (PyTorch 1.x, TF 1.x에서 유용)
# ENV TF_GPU_ALLOCATOR=cuda_malloc_async # TensorFlow 2.x에서 비동기 할당자 사용
# ENV TF_FORCE_GPU_ALLOW_GROWTH=true # TensorFlow 2.x에서 메모리 성장 강제
# 실제 애플리케이션 코드 내에서 PyTorch/TensorFlow API를 통해 제어하는 것이 더 권장됩니다.
# 예를 들어, PyTorch에서 0.8 비율로 메모리 프리-할당 (Pre-allocation)
# python -c "import torch; torch.cuda.set_per_process_memory_fraction(0.8, 0)"
# 이 예시는 컨테이너 실행 시 직접 환경 변수를 주입하는 방식입니다.
docker run --gpus '"device=0"' -e CUDA_VISIBLE_DEVICES=0 \
-e TF_FORCE_GPU_ALLOW_GROWTH=true \
your_ai_service_image:latest
CUDA_VISIBLE_DEVICES는 컨테이너 내부에서 보이는 GPU를 제한하며, TF_FORCE_GPU_ALLOW_GROWTH=true는 TensorFlow의 메모리 성장 옵션을 강제합니다. PyTorch의 경우 torch.cuda.set_per_process_memory_fraction() API를 통해 프로세스별 GPU 메모리 사용 비율을 명시적으로 설정할 수 있습니다.
실시간 GPU 메모리 모니터링 및 진단
OOM 문제 해결의 핵심은 현재 GPU 메모리 상태를 정확히 파악하는 것입니다. nvidia-smi는 가장 기본적인 도구이며, 이를 주기적으로 모니터링하여 메모리 사용 패턴을 분석해야 합니다.
# 1초 간격으로 GPU 사용량 및 메모리 상태를 모니터링합니다.
watch -n 1 nvidia-smi # 실시간으로 GPU 사용량과 메모리 상태를 확인하여 이상 패턴을 감지합니다.
# 특정 시점의 상세 프로세스별 GPU 메모리 사용량을 확인합니다.
nvidia-smi pmon # 프로세스별 GPU 메모리 사용량을 모니터링하여 메모리 누수나 과도한 점유를 진단합니다.
# GPU 메모리 사용량 이력을 로그로 남겨 분석합니다.
# (예: 10초마다 GPU 0번의 메모리 사용량을 파일로 저장)
while true; do nvidia-smi --query-gpu=timestamp,name,pci.bus_id,driver_version,utilization.gpu,utilization.memory,memory.total,memory.free,memory.used --format=csv -i 0 >> gpu_monitor_log.csv; sleep 10; done # GPU 메모리 사용 이력을 기록하여 장기적인 추세를 분석합니다.
nvidia-smi 출력에서 Used 메모리와 Free 메모리뿐만 아니라, Processes 섹션에서 각 프로세스가 점유하고 있는 메모리 양을 확인하는 것이 중요합니다. 만약 특정 프로세스가 비정상적으로 많은 메모리를 점유하거나, 해제되지 않는다면 메모리 누수를 의심할 수 있습니다.
사이드 이펙트 방지 트러블슈팅 FAQ
Q. torch.cuda.empty_cache()를 너무 자주 호출하면 성능에 영향이 있나요?
A. 네, 영향이 있을 수 있습니다. empty_cache()는 캐시된 메모리를 해제하고 OS에 반환하는 과정에서 약간의 오버헤드를 발생시킵니다. 따라서 모든 추론 요청마다 호출하기보다는, 배치 추론의 끝이나 서비스 유휴 시간, 또는 메모리 사용량이 급증하는 특정 시점에 호출하는 것이 좋습니다. 과도한 호출은 오히려 성능 저하를 야기할 수 있으므로, 실제 서비스 부하 테스트를 통해 최적의 호출 시점을 찾아야 합니다.
Q. TensorFlow의 set_memory_growth(gpu, True) 설정 후에도 OOM이 발생합니다. 다른 원인이 있을까요?
A. set_memory_growth(True)는 TensorFlow가 필요한 만큼만 메모리를 할당하도록 하지만, 이것이 메모리 단편화를 완전히 막지는 못합니다. 여전히 작은 텐서들이 빈번하게 할당/해제되면서 단편화가 발생할 수 있습니다. 이 경우, 모델 자체의 메모리 사용량 최적화(예: FP16 혼합 정밀도 사용, 모델 양자화), 배치 사이즈 조절, 또는 더 큰 GPU 메모리를 가진 하드웨어로의 업그레이드를 고려해야 합니다. 또한, 다른 프로세스가 GPU 메모리를 점유하고 있는지 nvidia-smi로 확인하는 것도 중요합니다.
Q. 여러 AI 모델을 하나의 GPU에서 서빙할 때 OOM이 자주 발생합니다. 어떻게 관리해야 할까요?
A. 여러 모델을 한 GPU에서 서빙하는 것은 메모리 단편화와 OOM의 주요 원인 중 하나입니다. 각 모델이 독립적으로 메모리를 할당하고 해제하기 때문입니다.
- 모델 로드/언로드 전략: 사용 빈도가 낮은 모델은 필요할 때만 로드하고, 사용 후에는 명시적으로 언로드하여 메모리를 해제해야 합니다. (예: PyTorch의
del model; torch.cuda.empty_cache()) - GPU 분할(Multi-Instance GPU, MIG): NVIDIA A100/H100 GPU는 MIG 기능을 제공하여 하나의 물리 GPU를 여러 개의 독립적인 GPU 인스턴스로 분할할 수 있습니다. 각 인스턴스는 자체적인 컴퓨팅 및 메모리 리소스를 가지므로, 모델 간의 메모리 간섭을 최소화하여 단편화를 줄일 수 있습니다.
- 메모리 풀링/할당자 최적화: 커스텀 메모리 할당자를 구현하거나, PyTorch의
torch.cuda.memory.set_allocator_config와 같은 API를 사용하여 메모리 할당 전략을 더 세밀하게 제어할 수 있습니다. - 컨테이너별 GPU 분리: 가능하다면 각 모델 서빙 컨테이너에 전용 GPU를 할당하는 것이 가장 안정적입니다.
--gpus '"device=X"'옵션을 사용하여 컨테이너별로 다른 GPU를 할당합니다.
AI 모델 서빙 환경에서 GPU 메모리 단편화로 인한 OOM 문제는 단순히 메모리 용량 부족이 아닌, 복합적인 메모리 관리 전략의 문제입니다. CUDA 메모리 할당 메커니즘을 이해하고, PyTorch/TensorFlow의 API를 활용하여 캐시를 관리하며, 컨테이너 환경에서 리소스 격리를 철저히 하는 것이 중요합니다. 또한, 지속적인 GPU 모니터링을 통해 메모리 사용 패턴을 분석하고, 문제가 발생했을 때 신속하게 진단할 수 있는 체계를 갖추는 것이 안정적인 AI 서비스 운영의 핵심입니다. 이 글에서 제시된 방법들이 여러분의 AI 서비스 안정화에 실질적인 도움이 되기를 바랍니다.
