GPU 사용 최적화를 위해 개선한 코드 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

1. 이미지 전처리 작업 병렬화

a. 변경 코드

  • 기존 코드

    for i in range(0, len(images), batch_size):
            batch_images = images[i:i + batch_size]
            preprocessed_batch = torch.stack([preprocess(img) for img in batch_images])
            preprocessed_batches.append(preprocessed_batch)
    
  • 수정한 코드

    # ThreadPoolExecutor 생성
    with ThreadPoolExecutor() as executor:
        # 전처리 함수를 lambda로 정의
        preprocess_func = lambda img: preprocess(img)
            
        for i in range(0, len(images), batch_size):
            batch_images = images[i:i + batch_size]
            # 병렬로 전처리 수행
            preprocessed_batch = list(executor.map(preprocess_func, batch_images))
            preprocessed_batch = torch.stack(preprocessed_batch)
            preprocessed_batches.append(preprocessed_batch)
    

b. GIL 환경에서도 병렬화 효과가 있나? → 있다!

preprocess(img)가 GIL을 해제하는 C/CUDA 연산 기반이면, ThreadPoolExecutor를 쓰는 병렬화는 실제로 성능 이점이 있다.

가. Python GIL과 C/CUDA 연산의 관계

  • Python은 인터프리터 수준의 코드(Python bytecode) 실행에는 GIL이 걸려 있음

  • 하지만 NumPy, PyTorch, PIL 등 C/C++로 구현된 연산GIL을 자동으로 해제

    # 예시: 아래 연산들은 대부분 GIL을 해제함
    torch.Tensor operations
    numpy.dot()
    PIL.Image.resize()
    
  • 즉, preprocess(img) 안에서 PIL.Image 변환, transforms.Resize, ToTensor, Normalize 같은 PyTorch Transform 사용 시

    내부적으로 C/C++ 또는 CUDA 연산 → GIL 해제 → 병렬 처리 가능

나. 서비스에서 사용하는 preprocess(OpenAI CLIP)

from torchvision import transforms

def _transform(n_px):
    return transforms.Compose([
        transforms.Resize(n_px, interpolation=Image.BICUBIC),
        transforms.CenterCrop(n_px),
        transforms.ToTensor(),
        transforms.Normalize(
            mean=[0.48145466, 0.4578275, 0.40821073],
            std=[0.26862954, 0.26130258, 0.27577711])
    ])
  • 각각의 Transform이 GIL을 어떻게 처리하나?

    Transform 내부 연산 GIL 해제 여부 비고
    Resize PIL.Image.resize (C기반) ✅ GIL 해제 빠름
    CenterCrop PIL + NumPy 슬라이싱 ✅ 대부분 해제 빠름
    ToTensor PIL → Tensor 변환 ✅ C 확장 모듈 빠름
    Normalize Tensor 연산 (C++/CUDA) ✅ GIL 해제 GPU에서도 효율적
  • 즉, 전체적으로 모두 GIL 해제를 유도하는 연산들이며, ThreadPoolExecutor를 사용하면 병렬 처리의 실효성이 매우 높음

다. 결론: 지금의 전처리 preprocess(img)는 병렬 처리하기에 적합한 작업

  • GIL 영향을 거의 받지 않음 (PIL + Tensor transform 기반)
  • ThreadPoolExecutor로 묶어도 context switching 비용보다 성능 이득이 큼
  • 특히 batch_size = 32 이상이면 체감 성능 향상 분명함