가이드 · 문제 해결

CUDA out of memory 오류 해결하기

GPU 메모리(VRAM)에 새 텐서를 올릴 공간이 없다는 오류예요. 메시지로 원인을 가린 뒤 다른 프로세스 정리, 그래디언트 끄기, 배치 축소와 혼합 정밀도 순서로 시도하세요.

업데이트
읽는 시간
약 11분

오류 메시지부터 읽어 보세요

버전에 따라 torch.OutOfMemoryError, torch.cuda.OutOfMemoryError, RuntimeError: CUDA out of memory로 이름만 다를 뿐 같은 오류예요. 최근 버전은 원인을 가려낼 숫자를 함께 보여 줘요.

오류 메시지 예시
torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 2.00 GiB. GPU 0 has a total capacity of 23.56 GiB of which 1.31 GiB is free. Including non-PyTorch memory, this process has 22.24 GiB memory in use. Of the allocated memory 19.80 GiB is allocated by PyTorch, and 1.92 GiB is reserved by PyTorch but unallocated. If reserved but unallocated memory is large try setting PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to avoid fragmentation.
24GB GPU에서 나온 예시예요. 숫자와 문구는 버전마다 달라요.
메시지에서 볼 부분
문구뜻의심할 원인
Tried to allocate이번에 실패한 요청 하나의 크기크면 큰 배치나 긴 시퀀스, 작으면 메모리가 이미 거의 찬 상태
this process has … in use내 프로세스 전체 사용량 (CUDA 컨텍스트 포함)전체 용량에 가까우면 내 코드
Process … has … in use같은 GPU를 쓰는 다른 프로세스 (표시될 때만)다른 노트북 커널, 끝나지 않은 학습
reserved by PyTorch but unallocatedPyTorch가 확보했지만 비어 있는 메모리크면 단편화

다른 프로세스와 노트북 커널을 확인하세요

이전 학습이 아직 돌고 있거나 다른 노트북 커널이 메모리를 붙잡고 있을 수 있어요. 출력 읽는 법은 nvidia-smi 사용법에 있어요.

bash
# GPU 메모리를 쓰는 프로세스 목록
nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv

# 필요 없는 프로세스를 종료해요 (12345는 위에서 확인한 PID)
kill 12345

Jupyter 커널은 살아 있는 동안 잡은 GPU 메모리를 돌려주지 않고, 탭을 닫아도 계속 돌 수 있어요. JupyterLab의 실행 중인 커널 목록에서 안 쓰는 커널을 종료하세요.

노트북에서 OOM이 한 번 나면 예외 정보와 셀 출력 기록(Out)이 텐서를 계속 붙잡아서, 변수를 지워도 메모리가 바로 풀리지 않을 수 있어요. 이럴 때는 커널을 재시작하는 게 가장 확실해요.

평가와 추론에서는 그래디언트를 끄세요

그래디언트를 끄지 않으면 평가 중에도 역전파용 중간 값을 모두 저장해서 학습만큼 메모리를 써요. 출력을 리스트에 모으면 계산 그래프까지 쌓여요. model.eval()은 드롭아웃과 배치 정규화 동작만 바꾸고, 그래디언트는 끄지 않아요.

python
model.eval()
with torch.inference_mode():  # torch.no_grad()도 같은 목적으로 쓸 수 있어요
    for x, y in val_loader:
        preds = model(x.cuda())

inference_mode()에서 만든 텐서는 나중에 그래디언트 계산에 쓸 수 없어요. 그래야 한다면 torch.no_grad()를 쓰세요.

학습 루프에서 total_loss += loss처럼 손실을 텐서째 더하면 스텝마다 계산 그래프가 남아 메모리가 조금씩 늘어요. 기록용으로는 loss.item()으로 숫자로 바꿔 더하세요.

학습 메모리 줄이기

배치를 줄이고 그래디언트를 누적해요

활성화 메모리는 배치 크기와 시퀀스 길이에 따라 늘어나요. 배치를 줄이고 그래디언트를 누적하면 유효 배치 크기는 그대로 두고 메모리만 줄일 수 있어요.

혼합 정밀도를 써요

torch.autocast는 가중치는 FP32로 두고 연산과 활성화 값을 16비트로 처리해요. Ampere 이후 GPU(torch.cuda.get_device_capability()가 (8, 0) 이상)라면 BF16을 쓰세요. BF16이 없는 V100 같은 GPU에서는 FP16을 쓰고, 그래디언트가 0으로 사라지지 않도록 GradScaler를 함께 써요.

python
accum_steps = 4  # 배치를 4분의 1로 줄였다면 4번 모아서 한 번 업데이트해요
optimizer.zero_grad()

for step, (x, y) in enumerate(train_loader):
    x, y = x.cuda(), y.cuda()
    with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
        loss = criterion(model(x), y) / accum_steps
    loss.backward()

    if (step + 1) % accum_steps == 0:
        optimizer.step()
        optimizer.zero_grad()
BF16 혼합 정밀도와 그래디언트 누적 예시예요.
python
scaler = torch.amp.GradScaler("cuda")

with torch.autocast(device_type="cuda", dtype=torch.float16):
    loss = criterion(model(x), y)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
FP16을 쓸 때의 예시예요. 오래된 PyTorch에서는 torch.cuda.amp.GradScaler()로 만들어요.

추론만 한다면 model.to(torch.bfloat16)로 가중치 자체를 16비트로 바꿔 가중치 메모리를 절반으로 줄일 수도 있어요.

그래디언트 체크포인팅을 켜요

순전파의 중간 값을 저장하지 않고 역전파 때 다시 계산해요. 시간은 더 들지만 활성화 메모리가 크게 줄어서, 배치를 더 줄이기 어려울 때 쓸 만해요.

python
# Hugging Face Transformers 모델
model.gradient_checkpointing_enable()

# 직접 만든 모델에서는 블록 단위로 감싸요
from torch.utils.checkpoint import checkpoint

def forward(self, x):
    for block in self.blocks:
        x = checkpoint(block, x, use_reentrant=False)
    return x

메모리 비우기와 단편화

PyTorch는 해제한 메모리를 캐시에 남겨 두고 다시 써요. 그래서 텐서를 지워도 nvidia-smi 사용량이 줄지 않는 게 정상이에요. 큰 텐서를 정리하려면 참조를 지우고 가비지 컬렉션을 돌린 뒤 캐시를 비워요.

python
import gc
import torch

del outputs, loss  # 큰 텐서를 가리키는 변수를 지워요
gc.collect()
torch.cuda.empty_cache()

reserved by PyTorch but unallocated 값이 크면 단편화예요. 빈 공간은 충분한데 잘게 나뉘어서 큰 텐서가 들어갈 자리가 없는 상태로, 배치나 시퀀스 길이가 매번 달라지는 작업에서 자주 생겨요. 실험적 옵션인 expandable_segments를 켜 보세요.

bash
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True python train.py

노트북에서는 import torch보다 먼저 os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"를 실행해야 적용돼요. 최신 버전은 PYTORCH_ALLOC_CONF라는 이름도 받는데, 오류 메시지가 안내하는 이름을 쓰면 돼요.

실제 사용량을 측정하세요

한 스텝을 실행한 뒤 최대 사용량을 확인하세요. max_memory_allocated()는 텐서가 실제로 차지한 최대치, memory_reserved()는 캐시를 포함해 PyTorch가 확보한 양이에요.

python
torch.cuda.reset_peak_memory_stats()

loss = criterion(model(x), y)
loss.backward()
optimizer.step()

gib = 1024**3
print(f"최대 할당: {torch.cuda.max_memory_allocated() / gib:.2f} GiB")
print(f"현재 확보: {torch.cuda.memory_reserved() / gib:.2f} GiB")
print(torch.cuda.memory_summary(abbreviated=True))

배치 크기를 바꿔 가며 재 보고, 긴 샘플이 섞여도 버틸 만큼 여유 있는 크기를 고르세요.

LLM 서빙에서 메모리가 부족할 때

vLLM은 시작할 때 GPU 메모리를 정해진 비율만큼 잡고, 가중치를 올린 뒤 남는 공간을 KV 캐시로 써요. 메모리 문제는 코드보다 서버 옵션으로 조절해요.

bash
vllm serve Qwen/Qwen2.5-7B-Instruct \
  --gpu-memory-utilization 0.85 \
  --max-model-len 8192 \
  --max-num-seqs 64
모델 이름과 값은 예시예요.
  • --gpu-memory-utilization: vLLM이 잡을 메모리 비율(0~1)이에요. 같은 GPU를 다른 프로세스와 나눠 쓰면 낮추고, KV 캐시 공간이 모자라다는 오류가 나면 조금 올려요.
  • --max-model-len: 입력과 출력을 합친 최대 길이예요. 지정하지 않으면 모델의 최대 길이를 써서, 긴 컨텍스트 모델은 시작부터 실패할 수 있어요. 실제로 필요한 길이로 줄이세요.
  • --max-num-seqs: 동시에 처리할 요청 수 상한이에요. 낮추면 처리량은 줄지만 메모리를 아껴요.

그래도 모자라면 양자화 모델을 쓰거나 KV 캐시 정밀도를 낮춰요. 모델 크기별 필요 메모리는 LLM VRAM 계산법을 보세요.

더 큰 GPU로 옮겨야 할 때

아래에 해당하면 설정을 더 줄이기보다 VRAM이 큰 GPU로 옮기는 편이 빨라요.

  • 배치 1, 혼합 정밀도, 그래디언트 체크포인팅을 모두 적용해도 부족해요.
  • 가중치만으로 VRAM 대부분이 차서 활성화 값이나 KV 캐시를 둘 공간이 거의 없어요.
  • 그래디언트 누적 횟수가 너무 커져서 학습이 감당하기 어려울 만큼 느려졌어요.
  • 양자화를 더 하면 결과 품질이 눈에 띄게 떨어져요.

옮기기 전에 LLM VRAM 계산기로 필요한 VRAM을 가늠해 보세요. Shelf에서 지금 만들 수 있는 가장 큰 GPU는 RTX 3090 24GB이고, AD103-32GB와 A100 80GB는 준비 중이에요. 데이터와 체크포인트를 볼륨에 두면 새 GPU 서버에 연결해 이어서 학습할 수 있어요.

자주 묻는 질문

nvidia-smi에는 메모리가 남아 있는데 왜 OOM이 나나요?

nvidia-smi는 측정 순간의 값이라 역전파 중 잠깐 치솟는 최대 사용량을 놓치기 쉬워요. 빈 공간이 잘게 나뉜 단편화 때문일 수도 있어요. 실제 최대치는 torch.cuda.max_memory_allocated()로 확인하세요.

배치 크기를 줄이면 학습 결과가 달라지나요?

그래디언트 누적으로 유효 배치 크기를 맞추면 대부분 비슷한 결과를 얻어요. 다만 배치 정규화처럼 배치 단위로 통계를 내는 층은 실제 배치 크기의 영향을 받아요.

GPU 서버가 필요할 때

필요한 GPU와 개발 환경을 골라 시간 단위로 시작해요.