CUDA 메모리 부족 오류, allocated·reserved·peak 로그는 어떻게 읽을까

CUDA 메모리 부족 오류를 읽을 때는 현재 텐서 사용량인 allocated, 할당기가 관리하는 reserved, 측정 구간의 최대 텐서 사용량인 peak를 같은 측정 구간·장치 기준으로 기록해야 합니다. 먼저 작업 전후 로그와 peak 초기화 위치를 확인하세요. reserved와 allocated를 더하거나, 둘의 차이를 곧바로 단편화로 해석하면 진단이 어긋납니다.

30초 진단 순서

  1. 종료 로그는 낮은데 OOM이 난다: 한 작업 직전 peak를 초기화하고, 작업 후 현재값과 peak를 함께 기록합니다.
  2. 반복할수록 allocated가 오른다: 같은 입력 크기에서 기준선을 비교하고 결과 텐서·참조가 남는 위치를 확인합니다. 이 패턴만으로 누수라고 확정하지 않습니다.
  3. reserved만 높아 보인다: 먼저 allocator backend와 로그 범위를 확인합니다. 무작정 empty_cache나 allocator 설정 변경부터 하지 않습니다.

적용 범위: PyTorch 2.14 공식 CUDA 문서, NVIDIA CUDA GPU를 쓰는 단일 프로세스의 단일 장치 진단입니다. 확인일은 2026-10-20 KST입니다. 아래 GPU 계측 코드는 문서 기반 미실행 예제이며, 실제로 실행한 부분은 GPU가 필요 없는 가상 로그 계산과 Python 문법 검사입니다. 다중 GPU·다중 프로세스·CUDA Graph 캡처 중인 서비스에 그대로 넣는 예제가 아닙니다.

목차와 한 줄 정의

  1. allocated·reserved: 현재 텐서 사용량과 캐싱 할당기의 관리량입니다.
  2. peak와 기준 구간: 마지막 초기화 이후 기록한 최대 텐서 사용량입니다.
  3. 작업 전후 계측: 같은 GPU에서 한 작업의 전후와 피크를 기록합니다.
  4. 가상 로그 해석: 종료 4 GiB와 구간 최대 9 GiB를 구분합니다.
  5. 증상별 다음 행동: 원인을 단정하지 않고 확인 범위를 좁힙니다.
  6. 요약·비교와 Appendix: 실행 여부와 제약을 다시 확인합니다.

1. allocated와 reserved는 더하는 숫자가 아니다

allocated는 해당 장치의 텐서가 현재 차지하는 메모리 바이트 수입니다. 이 값은 GPU 전체 사용량이 아닙니다. 공식 문서는 캐싱 할당기의 미사용 보유 메모리와 GPU 컨텍스트 때문에 nvidia-smi 표시와 다를 수 있다고 설명합니다. memory_allocated 공식 정의

reserved는 해당 장치에서 PyTorch 캐싱 할당기가 관리하는 메모리 바이트 수입니다. 일반적인 native backend의 그림에서는 allocated가 reserved 안에 들어갑니다. 예를 들어 reserved 10 GiB, allocated 4 GiB를 보고 총 14 GiB를 쓴다고 계산하면 안 됩니다. memory_reserved 공식 정의

단위도 맞춰야 합니다. API의 바이트 값을 1024²로 나누면 MiB, 1024³으로 나누면 GiB입니다. 이 글의 1 GiB는 1024 MiB입니다. 아래 가상 로그에서는 MiB를 사용하며, 제조사가 십진 GB로 표시하는 용량과 섞어 계산하지 않습니다.

2. peak는 ‘이번 작업의 추가 필요량’과 다르다

peak allocated는 측정 시작 이후 텐서가 가장 많이 차지했던 메모리입니다. 기본적으로 프로그램 시작 이후의 최댓값이므로 한 작업을 비교하려면 시작점을 다시 정해야 합니다. max_memory_allocated와 reset_peak_memory_stats가 이 용도로 쓰입니다.

모델을 올린 뒤 allocated가 이미 2 GiB이고 작업 중 peak가 9 GiB였다면, 관측한 최대 텐서 사용량은 9 GiB입니다. 그 차이 7 GiB는 시작값 대비 증가 폭이지, 어떤 입력에서도 보장되는 추가 용량이나 GPU 전체 필요량이 아닙니다. 실패한 할당 요청 크기까지 peak에 더해 기록된다고 가정해서도 안 됩니다.

초기화는 메모리를 비우는 동작이 아닙니다. peak 추적의 기준을 바꾸는 동작입니다. 다른 계측 코드가 같은 프로세스·장치의 peak를 보고 있다면 측정 구간에 간섭할 수 있으므로 독립된 재현 환경에서 사용하세요.

3. 한 작업의 전후 로그를 남기는 Python 예제

계측은 같은 장치·같은 조건에서 전후를 비교하는 절차입니다. 아래 코드는 작업 전에 동기화하고 peak를 초기화한 뒤 before·after를 출력합니다. OOM은 기록을 시도한 뒤 다시 전달하며 자동 재시도하지 않습니다. 이 코드를 실제 GPU에서 실행한 결과라고 주장하지 않습니다.

전제: 독자가 관리하는 CUDA 지원 PyTorch 환경, GPU 0 한 개, 동시에 같은 프로세스에서 GPU 작업을 추가하지 않는 진단 구간입니다. 아래 작은 텐서 예제를 실행하려면 gpu_memory_probe.py로 저장해 python gpu_memory_probe.py를 실행합니다. PyTorch 설치·드라이버 변경·유료 클라우드 작업은 이 글의 실행 검증 범위가 아닙니다.

Python · GPU 계측 예제 · 문법 검사만 수행, CUDA 실행 미검증

"""PyTorch 2.14 documentation-based CUDA example. GPU execution NOT tested here."""
import json
import torch

MIB = 1024 ** 2


def read_stats(tag, device=0):
    free_bytes, total_bytes = torch.cuda.memory.mem_get_info(device)
    return {
        "tag": tag,
        "device": device,
        "allocated_mib": round(torch.cuda.memory.memory_allocated(device) / MIB, 2),
        "reserved_mib": round(torch.cuda.memory.memory_reserved(device) / MIB, 2),
        "peak_allocated_mib": round(torch.cuda.memory.max_memory_allocated(device) / MIB, 2),
        "driver_free_mib": round(free_bytes / MIB, 2),
        "driver_total_mib": round(total_bytes / MIB, 2),
    }


def measure_step(step, device=0):
    if not torch.cuda.is_available():
        raise RuntimeError("CUDA GPU와 CUDA 지원 PyTorch가 필요합니다.")
    torch.cuda.synchronize(device)
    torch.cuda.memory.reset_peak_memory_stats(device)
    print(json.dumps(read_stats("before", device), ensure_ascii=False))
    try:
        result = step()
        torch.cuda.synchronize(device)
    except torch.OutOfMemoryError:
        try:
            print(json.dumps(read_stats("oom", device), ensure_ascii=False))
        except Exception as log_error:
            print("OOM 로그 수집 실패:", type(log_error).__name__)
        raise
    print(json.dumps(read_stats("after", device), ensure_ascii=False))
    return result


if __name__ == "__main__":
    print(json.dumps({"torch": torch.__version__, "cuda_runtime": torch.version.cuda}))
    if not torch.cuda.is_available():
        raise SystemExit("CUDA 사용 불가: GPU 예제는 여기서 종료합니다.")
    print("allocator:", torch.cuda.memory.get_allocator_backend())
    value = measure_step(lambda: torch.empty((256, 1024), device="cuda:0", dtype=torch.float32))

기본 예제의 텐서 원소 크기는 256 × 1024 × 4 bytes = 1 MiB입니다. 이는 텐서 데이터 크기의 계산이지 CUDA 컨텍스트·allocator의 전체 증가량을 예측한 값이 아닙니다. 자신의 코드에서는 measure_step에 한 번의 학습·추론 단계를 수행하는 함수를 전달하되 입력·출력 텐서의 수명을 기록해야 합니다. 반환값을 보유한 채 after를 찍기 때문에 그 결과 텐서도 현재 allocated에 남을 수 있습니다.

synchronize는 장치의 모든 stream에 있는 커널 완료를 기다립니다. 진단 구간을 구분하기 위한 것이며 평소 처리량을 측정하는 벤치마크 루프에 무심코 넣으면 동작과 성능을 바꿉니다. 여러 stream의 실제 서비스나 Graph 캡처 중에는 그대로 쓰지 마세요. synchronize 공식 문서

추가로 기록한 driver_free_mib는 cudaMemGetInfo가 반환하는 장치 전체 free/total 범위입니다. 이 프로세스의 allocated·reserved와 같은 범위가 아니며, 차례로 읽은 값은 원자적인 한 장면도 아닙니다. 다음 할당 성공 여부를 이 값 하나로 보장할 수 없습니다. mem_get_info

4. 종료 4 GiB, peak 9 GiB인 로그를 읽어 보자

가상 로그는 계산법을 설명하기 위해 만든 입력입니다. 실제 장치 출력이나 위 작은 텐서 예제의 예상 출력이 아닙니다. native backend의 단순화한 예로 시작 allocated 2 GiB, 종료 allocated 4 GiB, 종료 reserved 10 GiB, 구간 peak allocated 9 GiB를 사용합니다.

시작 · allocated 2 GiB

기존 텐서를 보유한 상태에서 peak 기준 설정

↓
구간 최대 · peak allocated 9 GiB

중간 텐서가 많아진 시점의 기록값

↓
종료 · allocated 4 GiB

중간 피크가 지나간 뒤의 현재값

종료 reserved 10 GiB 안에 allocated 4 GiB 포함

차이 6 GiB ≠ 확정 단편화 크기
차이 6 GiB ≠ 다음 요청에 보장된 가용량

자체 제작 가상 도식. 중간 최대값과 종료값을 더하지 않습니다. 단순화한 텐서 사용량 흐름이며 GPU 전체 메모리 지도나 실제 측정 차트가 아닙니다.

아래 계산 예제는 PyTorch를 import하지 않는 순수 Python입니다. Windows x64의 Python 실행 결과를 제시하며 가상 입력의 산술만 검증했습니다.

Python · analyze_demo.py · 가상 입력 산술 검증

"""CPU-only arithmetic on explicitly synthetic CUDA-style logs. No torch import."""
before = {"allocated_mib": 2048, "reserved_mib": 4096, "peak_allocated_mib": 2048}
after = {"allocated_mib": 4096, "reserved_mib": 10240, "peak_allocated_mib": 9216}

allocated_growth = after["allocated_mib"] - before["allocated_mib"]
peak_above_start = after["peak_allocated_mib"] - before["allocated_mib"]
peak_above_end = after["peak_allocated_mib"] - after["allocated_mib"]
reserved_gap = after["reserved_mib"] - after["allocated_mib"]

assert allocated_growth == 2048
assert peak_above_start == 7168
assert peak_above_end == 5120
assert reserved_gap == 6144
assert after["peak_allocated_mib"] / 1024 == 9
assert 1024 ** 3 / (1024 ** 2) == 1024
assert after["reserved_mib"] >= after["allocated_mib"]

print("입력: 가상 로그, GPU 실측 아님")
print(f"allocated 증가: {allocated_growth} MiB ({allocated_growth / 1024:.2f} GiB)")
print(f"구간 peak: {after['peak_allocated_mib']} MiB ({after['peak_allocated_mib'] / 1024:.2f} GiB)")
print(f"peak - 시작 allocated: {peak_above_start} MiB ({peak_above_start / 1024:.2f} GiB)")
print(f"끝 reserved - allocated: {reserved_gap} MiB ({reserved_gap / 1024:.2f} GiB)")
print("판정: 단편화·다음 할당 성공 여부는 이 로그만으로 미확인")

실제로 실행한 계산 출력 · GPU 실측 아님

입력: 가상 로그, GPU 실측 아님
allocated 증가: 2048 MiB (2.00 GiB)
구간 peak: 9216 MiB (9.00 GiB)
peak - 시작 allocated: 7168 MiB (7.00 GiB)
끝 reserved - allocated: 6144 MiB (6.00 GiB)
판정: 단편화·다음 할당 성공 여부는 이 로그만으로 미확인

이 예에서는 종료값만 보면 4 GiB지만 구간 최대는 9 GiB입니다. peak와 종료값의 차이는 5 GiB입니다. 따라서 종료 시 스냅샷만 보고 여유가 충분했다고 결론내릴 수 없습니다. 반대로 reserved−allocated가 6 GiB라는 사실만으로 그 공간이 모두 즉시 반환 가능하다거나 그만큼 단편화됐다고 결론내리지도 않습니다.

5. 패턴별로 한 가지씩 확인한다

  • allocated 기준선이 계속 오른다: 입력 크기와 반복 조건을 고정해 로그를 비교하고, 보관 중인 출력·참조를 확인합니다. 의도한 캐시나 워밍업일 수도 있으므로 증가만으로 누수 판정은 하지 않습니다.
  • 현재값은 비슷한데 peak만 크다: 입력 길이·배치·작업 단계를 기록합니다. 한 변수를 바꿔 피크가 어떻게 달라지는지 다시 확인합니다. 성능·정확도까지 함께 검수해야 하며 배치 축소가 모든 문제의 정답은 아닙니다.
  • reserved와 allocated 차이가 크다: backend를 확인하고 필요할 때 torch.cuda.memory_summary(device=0) 또는 allocator snapshot으로 블록 상태를 더 봅니다. 단편화는 후보이지 이 차이만으로 확정된 원인이 아닙니다.
  • PyTorch 로그와 전체 장치 사용량이 크게 다르다: 장치·프로세스 범위와 PyTorch 밖의 할당을 확인합니다. 다른 사용자의 프로세스를 임의로 종료하지 않습니다.

empty_cache는 살아 있는 텐서를 지우지 않습니다. 공식 문서는 미사용 캐시를 반환하는 동작과 텐서가 점유한 메모리를 구분합니다. allocator 옵션도 첫 조치가 아닙니다. 특히 max_split_size_mb는 native backend의 inactive split 블록 근거가 있는 경계 사례에서 검토하는 마지막 수단으로 안내되며, 성능 비용이 생길 수 있습니다. 이 글은 해당 설정을 자동 변경하거나 고정 값을 권하지 않습니다. PyTorch 메모리 관리

고급 snapshot도 모든 CUDA 할당을 보여주는 것은 아닙니다. 공식 메모리 시각화 문서는 PyTorch allocator를 통하지 않는 할당이 보이지 않을 수 있음을 설명합니다. 기록 파일에는 모델·코드 경로 같은 운영 정보가 포함될 수 있으므로 승인 없이 외부에 업로드하지 마세요. Understanding CUDA Memory Usage

6. 요약·비교: 현재값, 관리량, 구간 최대를 분리한다

로그 항목먼저 답할 질문
allocated지금 텐서가 얼마를 차지하나?
reserved할당기가 얼마를 관리하나?
peak allocated기준 구간 중 얼마나 높아졌나?
driver free장치 전체 free 범위는 어떤가?

안전한 순서는 환경·backend 기록 → 같은 장치에서 시작점 설정 → 한 작업의 before·after·peak 기록 → 한 변수씩 바꿔 재확인입니다. 로그가 알려주는 범위보다 더 큰 결론을 내리지 않는 것이 핵심입니다. peak를 줄였더라도 처리량과 응답 지연이 좋아졌다고 자동으로 결론내릴 수는 없습니다.

관련 개념은 같은 언어 글타래로 이어 볼 수 있습니다. 브라우저 메모리와 서버 KV Cache를 나누어 보는 글은 측정 위치를 구분하는 데, TTFT·TPOT·ITL과 처리량 설명은 메모리 변경 후 응답 품질을 무엇으로 검증할지 정하는 데 도움이 됩니다. 서로 다른 자원·성능 지표를 한 숫자로 대체하지 마세요.

Appendix · 실행 환경, 실패 조건과 출처

확인일 2026-10-20 KST. 문서 기준은 PyTorch 2.14입니다. GPU 계측 코드는 CUDA 지원 환경이 필요하고, CUDA 미지원·잘못된 장치·OOM·동시 작업·서로 다른 allocator backend에서는 실행 또는 해석에 제약이 생깁니다. 특히 일부 allocator 통계는 backend에 따라 의미가 달라집니다. 현재 allocator backend 확인

실행 여부: 가상 로그 계산은 Windows x64의 Python 3.14.3으로 실행하고 일곱 산술 조건을 확인했습니다. GPU 코드는 Python 문법만 검사했으며 PyTorch import·CUDA 실행·실제 LLM·처리량·메모리 누수 재현은 수행하지 않았습니다. 실제 GPU 출력이나 OOM 해결을 보장하는 글이 아닙니다. 설치 패키지·드라이버·전역 설정은 변경하지 않았습니다.

댓글

이 블로그의 인기 게시물

소니 A6000 Wi-Fi 전송 (컴퓨터로 보내기)

C#, cmd 명령어 창 안보이게 실행하기

SVN 서버 설치 및 웹 접속 설정-Ubuntu 14.04