본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 가비지 컬렉션(GC) 수동 제어로 딥러닝 메모리 누수 해결하는 7가지 방법

by Papa Martino V 2026. 4. 14.
728x90

Generational GC
Generational GC

파이썬은 레퍼런스 카운팅(Reference Counting)세대별 가비지 컬렉션(Generational GC)을 통해 메모리를 자동 관리합니다. 하지만 대규모 파라미터를 다루는 딥러닝 환경에서는 이 자동화 기능이 오히려 독이 될 때가 많습니다. 특히 GPU 메모리와 CPU 메모리 사이의 동기화 문제, 그리고 텐서 객체의 복잡한 순환 참조는 OOM(Out of Memory)의 주범이 됩니다. 본 포스팅에서는 GC 수동 제어의 실효성을 분석하고, 실무에서 메모리 누수를 완벽히 차단하는 고급 기법을 상세히 다룹니다.


1. 파이썬 GC 메커니즘과 딥러닝 메모리 관리의 차이점

일반적인 파이썬 애플리케이션과 달리 딥러닝 학습 루프는 수만 번의 반복(Iteration) 동안 거대한 행렬 연산을 수행합니다. 파이썬의 기본 GC는 객체가 '어느 정도' 쌓여야 작동하는데, 딥러닝에서는 그 '어느 정도'가 차기 전에 메모리가 포화 상태에 이르는 경우가 허다합니다.

비교 항목 자동 가비지 컬렉션 (Default) 수동 GC 제어 (Manual)
작동 시점 객체 할당/해제 임계치 도달 시 학습 루프의 특정 스텝(Step) 직후
메모리 오버헤드 낮으나 순간적인 메모리 스파이크 발생 GC 실행 중 일시적 연산 정지(Stop-the-world)
순환 참조 해결 지연 처리 (시간차 발생) 즉각적인 세대별 검사 및 해제
딥러닝 적합성 소규모 모델 및 추론(Inference)에 적합 대규모 트레이닝 및 멀티 GPU 환경 필수

2. 실무에서 즉시 적용 가능한 메모리 관리 Example 7가지

단순히 `gc.collect()`를 호출하는 것 이상의, 엔지니어링 관점에서의 메모리 누수 해결 방법론을 코드와 함께 제시합니다.

Example 01. 학습 루프(Epoch) 종료 후 주기적 GC 호출

가장 기본적이면서도 확실한 방법입니다. 데이터 로더가 한 바퀴 돌 때마다 잔여 임시 객체를 강제로 비웁니다.

import gc
import torch

for epoch in range(num_epochs):
    for data in dataloader:
        train_step(data)
    
    # 에폭 종료 후 명시적 캐시 비우기 및 GC 실행
    torch.cuda.empty_cache() # GPU 캐시 우선 정리
    gc.collect()             # CPU 메모리 순환 참조 정리

Example 02. 가비지 컬렉션 임계치(Threshold) 수정하기

GC가 더 자주 발생하도록 기본 임계값을 낮추어 메모리 스파이크를 방지하는 방법입니다.

import gc

# 기본값 (700, 10, 10)에서 첫 세대 임계치를 낮춤
gc.set_threshold(400, 5, 5)
print(f"새로운 GC 임계치: {gc.get_threshold()}")

Example 03. Context Manager를 이용한 국소적 GC 비활성화

중요 연산 중에는 GC가 개입하지 못하게 막고, 연산이 끝나면 즉시 수거하도록 설계합니다. 이는 학습 속도 향상에도 기여합니다.

import gc

class GCFreeContext:
    def __enter__(self):
        gc.disable()
    def __exit__(self, exc_type, exc_val, exc_tb):
        gc.enable()
        gc.collect()

with GCFreeContext():
    # 극심한 메모리 연산이 일어나는 코드 블록
    heavy_tensor_operation()

Example 04. 디버그 모드를 활용한 누수 객체 추적

어떤 객체가 해제되지 않고 남아있는지 추적하여 코드 상의 구조적 결함을 해결합니다.

import gc

# 수거되지 않는 객체 목록 확인 모드 활성화
gc.set_debug(gc.DEBUG_LEAK)

# 특정 로직 수행 후
gc.collect()

for obj in gc.garbage:
    print(f"누수 의심 객체: {type(obj)} - ID: {id(obj)}")

Example 05. 대규모 텐서 삭제 후 명시적 참조 제거

변수를 `del`로 지워도 참조 카운트가 남는 경우가 많습니다. 이를 `None`으로 덮어쓰고 GC를 트리거합니다.

import torch
import gc

large_tensor = torch.randn(10000, 10000).cuda()
# ... 연산 수행 ...

del large_tensor
# 명시적으로 GC를 호출하여 GPU 포인터와 연결된 파이썬 객체 완전 해제
gc.collect()
torch.cuda.empty_cache()

Example 06. 순환 참조를 방지하는 약한 참조(weakref) 활용

모델의 레이어 간 참조가 복잡할 때, 강한 참조 대신 약한 참조를 사용하여 GC가 자동으로 수거할 수 있는 길을 열어줍니다.

import weakref
import gc

class ModelComponent:
    def __init__(self, parent):
        self.parent_ref = weakref.ref(parent)

# parent 객체가 소멸되면 parent_ref는 자동으로 None이 됨

Example 07. 멀티프로세싱(DDP) 환경에서의 개별 프로세스 GC 관리

PyTorch의 DistributedDataParallel 사용 시, 각 프로세스마다 독립적인 GC 스케줄을 가져야 좀비 메모리가 남지 않습니다.

import gc
import os

def train_worker(rank):
    # 각 프로세스 시작 시 GC 초기화
    gc.collect()
    # 학습 중 메모리 관리 로직...
    if rank == 0:
        print("Master process cleaning up...")
        gc.collect()

3. 전문가 결론: 수동 제어는 해결책인가?

질문에 대한 답은 "매우 효과적이지만, 전략적으로 사용해야 한다"입니다. 무분별한 `gc.collect()` 호출은 CPU 연산 자원을 소모하여 학습 속도를 10~20% 저하시킬 수 있습니다. 하지만 순환 참조가 빈번한 복잡한 모델 아키텍처나, 메모리 여유가 거의 없는 환경에서는 수동 제어만이 시스템 중단을 막는 유일한 방법입니다. 가장 권장되는 방식은 1) GPU 캐시 정리와 2) 세대별 GC 임계치 조정을 병행하는 것이며, 최후의 수단으로 학습 루프 말단에 수동 수거 로직을 배치하는 것입니다.

출처 및 참고문헌:

  • Python Documentation: 'gc' module internal (CPython 3.11+)
  • PyTorch Memory Management Guide (Memory Management Essentials)
  • High Performance Python 
728x90