🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Batch Inference 최적화 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2026. 2. 2. · 2 Views

Batch Inference 최적화 완벽 가이드

대량의 데이터를 효율적으로 처리하는 배치 추론의 핵심 기법을 다룹니다. GPU 메모리 관리부터 병렬 처리 전략까지, 실무에서 바로 적용할 수 있는 최적화 노하우를 익힐 수 있습니다.


목차

  1. 대량_처리의_필요성
  2. 배치_추론_vs_단일_추론
  3. 메모리_효율적인_배치_크기
  4. GPU_활용률_최적화
  5. 병렬_처리_전략
  6. 실전_100개_문장_배치_생성

1. 대량 처리의 필요성

김개발 씨는 음성 합성 서비스를 개발하고 있었습니다. 처음에는 사용자 요청이 하나씩 들어와서 순서대로 처리하면 됐는데, 어느 날 마케팅팀에서 긴급 요청이 들어왔습니다.

"내일까지 고객 안내 음성 10만 개를 만들어야 해요!"

배치 처리는 여러 개의 작업을 한꺼번에 묶어서 처리하는 방식입니다. 마치 택배 기사님이 한 집씩 방문하는 대신 같은 아파트 단지 물건을 한 번에 배송하는 것과 같습니다.

이 방식을 제대로 이해하면 처리 시간을 획기적으로 단축할 수 있습니다.

다음 코드를 살펴봅시다.

# 단일 처리: 하나씩 순차적으로 처리
def process_single(texts):
    results = []
    for text in texts:
        # 매번 모델을 호출하고 결과를 기다림
        result = model.generate(text)
        results.append(result)
    return results

# 배치 처리: 여러 개를 한 번에 처리
def process_batch(texts, batch_size=32):
    results = []
    # 텍스트를 batch_size 단위로 묶어서 처리
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i + batch_size]
        # 한 번의 호출로 여러 결과를 생성
        batch_results = model.generate_batch(batch)
        results.extend(batch_results)
    return results

김개발 씨는 입사 6개월 차 주니어 개발자입니다. TTS 서비스를 담당하며 매일 수백 건의 음성 합성 요청을 처리하고 있었습니다.

그런데 오늘은 상황이 달랐습니다. 마케팅팀에서 대규모 캠페인을 위해 10만 개의 개인화된 안내 음성이 필요하다고 했습니다.

"10만 개요? 지금 방식대로면..." 김개발 씨가 빠르게 계산해봤습니다.

현재 시스템은 음성 하나를 생성하는 데 평균 0.5초가 걸립니다. 10만 개면 50,000초, 약 14시간입니다.

내일 아침까지는 불가능한 시간이었습니다. 선배 개발자 박시니어 씨가 김개발 씨의 걱정스러운 표정을 보고 다가왔습니다.

"뭐가 문제야?" 상황을 들은 박시니어 씨는 빙그레 웃었습니다. "배치 처리를 써보는 건 어때?" 그렇다면 배치 처리란 정확히 무엇일까요?

쉽게 비유하자면, 배치 처리는 마치 뷔페 요리사와 같습니다. 일반 레스토랑에서는 손님이 주문할 때마다 요리를 한 접시씩 만듭니다.

하지만 뷔페에서는 같은 요리를 한 번에 대량으로 만들어 놓습니다. 재료 손질, 조리 기구 예열, 설거지 등의 작업을 한 번만 하면 되니까 훨씬 효율적입니다.

컴퓨터 세계에서도 마찬가지입니다. 데이터를 하나씩 처리하면 매번 오버헤드가 발생합니다.

모델을 메모리에 올리고, GPU와 통신하고, 결과를 저장하는 과정이 반복됩니다. 하지만 여러 데이터를 한꺼번에 묶어서 처리하면 이런 오버헤드를 한 번만 감당하면 됩니다.

배치 처리가 없던 시절에는 어땠을까요? 초기 머신러닝 서비스들은 요청이 들어올 때마다 하나씩 처리했습니다.

트래픽이 적을 때는 문제가 없었습니다. 하지만 서비스가 성장하면서 요청이 폭증하면 서버가 버티지 못했습니다.

더 큰 문제는 비용이었습니다. GPU는 비싼 자원인데, 하나씩 처리하면 GPU 활용률이 10%도 안 되는 경우가 많았습니다.

바로 이런 문제를 해결하기 위해 배치 추론이라는 개념이 정립되었습니다. 배치 추론을 사용하면 처리량이 획기적으로 증가합니다.

위의 코드에서 볼 수 있듯이, 단일 처리는 텍스트 하나마다 model.generate()를 호출합니다. 반면 배치 처리는 32개를 묶어서 한 번에 model.generate_batch()를 호출합니다.

실제로 측정해보면 놀라운 결과가 나옵니다. 단일 처리로 1000개를 처리하는 데 500초가 걸렸다면, 배치 처리로는 50초도 안 걸릴 수 있습니다.

10배 이상의 성능 향상입니다. 하지만 주의할 점도 있습니다.

배치 크기를 너무 크게 잡으면 메모리가 부족해질 수 있습니다. 또한 실시간 응답이 필요한 서비스에서는 배치를 모을 때까지 기다리는 지연 시간이 문제가 될 수 있습니다.

상황에 맞는 적절한 배치 크기를 찾는 것이 중요합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 조언을 듣고 배치 처리를 적용한 결과, 10만 개의 음성을 2시간 만에 생성할 수 있었습니다. 마케팅팀도 만족하고, 김개발 씨도 한 단계 성장한 느낌이었습니다.

실전 팁

💡 - 배치 처리는 대량 데이터 처리의 기본이므로 반드시 익혀두세요

  • 실시간 서비스에서는 마이크로 배칭을 고려해보세요

2. 배치 추론 vs 단일 추론

김개발 씨는 배치 처리의 개념을 이해했지만, 정확히 어떤 차이가 있는지 숫자로 확인하고 싶었습니다. 박시니어 씨가 화이트보드에 두 방식을 비교하는 그림을 그리기 시작했습니다.

"직접 비교해보면 왜 배치가 빠른지 확실히 알 수 있을 거야."

단일 추론은 입력 하나당 모델을 한 번 호출하는 방식입니다. 배치 추론은 여러 입력을 묶어서 모델을 한 번 호출합니다.

핵심 차이는 모델 호출 횟수와 GPU 활용률에 있습니다.

다음 코드를 살펴봅시다.

import time
import torch

# 단일 추론 방식
def single_inference(model, texts):
    start = time.time()
    results = []
    for text in texts:
        # 매 호출마다 GPU 커널 실행 오버헤드 발생
        with torch.no_grad():
            output = model(text)
        results.append(output)
    print(f"단일 추론: {time.time() - start:.2f}초")
    return results

# 배치 추론 방식
def batch_inference(model, texts, batch_size=32):
    start = time.time()
    results = []
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i + batch_size]
        # GPU 병렬 처리로 한 번에 여러 결과 생성
        with torch.no_grad():
            outputs = model(batch)
        results.extend(outputs)
    print(f"배치 추론: {time.time() - start:.2f}초")
    return results

박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다. 왼쪽에는 작은 상자가 일렬로 100개 나열되어 있고, 오른쪽에는 큰 상자 4개가 그려져 있었습니다.

"왼쪽이 단일 추론이야. 100개 데이터를 처리하려면 모델을 100번 호출해야 해." 박시니어 씨가 설명을 이어갔습니다.

"오른쪽이 배치 추론이야. 배치 크기가 32라면 모델을 4번만 호출하면 돼." 김개발 씨가 고개를 갸웃거렸습니다.

"그래도 결국 100개를 처리하는 건 같잖아요. 왜 속도 차이가 나는 거예요?" 좋은 질문이었습니다.

핵심은 오버헤드병렬 처리에 있습니다. 먼저 오버헤드를 살펴보겠습니다.

모델을 한 번 호출할 때마다 여러 가지 준비 작업이 필요합니다. CPU에서 GPU로 데이터를 전송하고, GPU 커널을 실행하고, 결과를 다시 CPU로 가져옵니다.

이 과정이 매번 0.001초씩 걸린다고 가정해봅시다. 100번 호출하면 0.1초, 4번 호출하면 0.004초입니다.

오버헤드만 25배 차이가 납니다. 더 중요한 건 GPU의 특성입니다.

GPU는 수천 개의 작은 코어로 구성되어 있습니다. 마치 개미 군단과 같습니다.

한 마리 개미는 약하지만, 수천 마리가 동시에 일하면 엄청난 양의 일을 해낼 수 있습니다. 단일 추론은 이 개미 군단에게 일을 하나씩 시키는 것과 같습니다.

한 마리 개미가 열심히 일하는 동안 나머지 수천 마리는 놀고 있습니다. GPU 활용률이 바닥을 기는 이유입니다.

반면 배치 추론은 개미 군단 전체에게 동시에 일을 시킵니다. 32개의 데이터가 들어오면 GPU의 수천 코어가 동시에 각각의 데이터를 처리합니다.

이것이 병렬 처리의 힘입니다. 위의 코드를 자세히 살펴보겠습니다.

single_inference 함수에서는 for 문 안에서 매번 model(text)를 호출합니다. 반면 batch_inference 함수에서는 여러 텍스트를 리스트로 묶어서 model(batch)를 호출합니다.

실제로 1000개의 문장을 처리한다고 가정해봅시다. 단일 추론으로는 약 100초가 걸렸다면, 배치 크기 32의 배치 추론으로는 약 8초만 걸릴 수 있습니다.

12배 이상의 성능 향상입니다. 하지만 모든 상황에서 배치 추론이 좋은 것은 아닙니다.

실시간 채팅 봇처럼 즉각적인 응답이 필요한 경우에는 배치를 모으느라 기다리는 시간이 더 문제가 될 수 있습니다. 이런 경우에는 동적 배칭이나 마이크로 배칭 기법을 사용합니다.

김개발 씨가 이해했다는 듯 고개를 끄덕였습니다. "그러니까 GPU를 제대로 활용하려면 일감을 한꺼번에 몰아줘야 하는 거군요!"

실전 팁

💡 - nvidia-smi 명령어로 GPU 활용률을 모니터링하며 최적의 배치 크기를 찾으세요

  • 배치 크기가 크다고 항상 좋은 것은 아니며, 메모리와 지연 시간을 고려해야 합니다

3. 메모리 효율적인 배치 크기

김개발 씨가 신나서 배치 크기를 512로 설정하고 실행했습니다. 그런데 갑자기 화면에 무시무시한 에러가 나타났습니다.

"CUDA out of memory"라는 빨간 글씨를 보는 순간, 심장이 쿵 내려앉았습니다.

배치 크기를 결정할 때는 GPU 메모리 용량을 반드시 고려해야 합니다. 배치 크기가 커질수록 필요한 메모리도 선형적으로 증가합니다.

최적의 배치 크기는 메모리를 최대한 활용하면서도 OOM 에러가 발생하지 않는 지점입니다.

다음 코드를 살펴봅시다.

import torch

def find_optimal_batch_size(model, sample_input, max_batch=256):
    """메모리 에러 없이 사용 가능한 최대 배치 크기 탐색"""
    batch_size = 1
    optimal_size = 1

    while batch_size <= max_batch:
        try:
            # 테스트 배치 생성
            test_batch = [sample_input] * batch_size
            torch.cuda.empty_cache()  # GPU 메모리 정리

            with torch.no_grad():
                _ = model(test_batch)

            optimal_size = batch_size
            print(f"배치 크기 {batch_size}: 성공")
            batch_size *= 2  # 2배씩 증가하며 탐색

        except RuntimeError as e:
            if "out of memory" in str(e):
                print(f"배치 크기 {batch_size}: 메모리 부족")
                break
            raise e

    # 안전 마진 20% 적용
    return int(optimal_size * 0.8)

박시니어 씨가 김개발 씨의 화면을 보며 웃음을 참았습니다. "CUDA out of memory, 다들 한 번씩은 겪는 통과의례야." 김개발 씨는 억울했습니다.

"배치 크기가 클수록 좋다고 하셨잖아요!" "크면 좋긴 한데, GPU 메모리라는 한계가 있어." 박시니어 씨가 설명을 시작했습니다. GPU 메모리를 수영장에 비유해봅시다.

수영장 크기는 정해져 있습니다. RTX 3090이라면 24GB, A100이라면 40GB 또는 80GB입니다.

이 수영장에 데이터라는 물을 채워야 합니다. 배치 크기가 32라면 32명이 동시에 수영장에 들어가는 것과 같습니다.

충분히 여유가 있습니다. 하지만 배치 크기를 512로 늘리면?

512명이 한꺼번에 뛰어들면 수영장이 넘칩니다. 이것이 바로 OOM(Out of Memory) 에러입니다.

그렇다면 최적의 배치 크기는 어떻게 찾을까요? 위의 코드에서 보여주는 방법이 가장 실용적입니다.

이진 탐색 방식으로 배치 크기를 1부터 시작해서 2배씩 늘려갑니다. 1, 2, 4, 8, 16, 32, 64...

이런 식으로 메모리 에러가 날 때까지 시도합니다. 코드의 핵심 부분을 살펴보겠습니다.

torch.cuda.empty_cache()는 GPU 메모리에서 사용하지 않는 캐시를 정리합니다. 이전 테스트에서 남은 찌꺼기를 청소하는 것입니다.

그 다음 model(test_batch)를 실행해서 실제로 그 배치 크기가 처리 가능한지 확인합니다. 메모리 에러가 발생하면 while 문을 빠져나오고, 마지막으로 성공한 배치 크기를 반환합니다.

여기서 중요한 건 안전 마진입니다. 코드에서 0.8을 곱하는 이유는 실제 서비스에서는 다양한 길이의 입력이 들어오기 때문입니다.

테스트할 때는 짧은 문장으로 했는데, 실제로 긴 문장이 들어오면 메모리 사용량이 더 늘어납니다. 그래서 20% 정도의 여유를 두는 것이 안전합니다.

실무에서는 몇 가지 추가 기법도 사용합니다. 혼합 정밀도(Mixed Precision) 학습을 사용하면 메모리 사용량을 절반으로 줄일 수 있습니다.

그래디언트 체크포인팅을 사용하면 학습 시 메모리를 더 아낄 수 있습니다. 김개발 씨가 코드를 실행해봤습니다.

결과는 배치 크기 64가 최적이었습니다. 안전 마진을 적용하면 51, 보수적으로 48이나 32를 사용하면 안전할 것 같았습니다.

"64까지 되는구나. 생각보다 넉넉하네요!" 김개발 씨가 안도의 한숨을 쉬었습니다.

실전 팁

💡 - 프로덕션 환경에서는 테스트 시보다 20-30% 작은 배치 크기를 사용하세요

  • torch.cuda.memory_summary()로 상세한 메모리 사용 현황을 확인할 수 있습니다

4. GPU 활용률 최적화

김개발 씨가 nvidia-smi를 실행해봤더니, GPU 활용률이 30%밖에 안 됐습니다. 배치 처리를 적용했는데도 GPU가 놀고 있다니, 뭔가 잘못된 것 같았습니다.

박시니어 씨는 "CPU와 GPU 사이에 병목이 있는 것 같네"라고 말했습니다.

GPU 활용률을 높이려면 데이터 전처리와 후처리 과정도 최적화해야 합니다. CPU에서 데이터를 준비하는 동안 GPU가 기다리는 상황을 피하기 위해 비동기 데이터 로딩파이프라이닝을 사용합니다.

다음 코드를 살펴봅시다.

import torch
from torch.utils.data import DataLoader
from concurrent.futures import ThreadPoolExecutor

class OptimizedBatchProcessor:
    def __init__(self, model, batch_size=32, num_workers=4):
        self.model = model
        self.batch_size = batch_size
        # 데이터 전처리를 위한 스레드 풀
        self.executor = ThreadPoolExecutor(max_workers=num_workers)
        # GPU 메모리에 미리 고정된 버퍼 사용
        self.pin_memory = torch.cuda.is_available()

    def preprocess_async(self, texts):
        """CPU에서 비동기로 전처리 수행"""
        futures = [self.executor.submit(self._preprocess, t) for t in texts]
        return [f.result() for f in futures]

    def process(self, texts):
        # CPU: 다음 배치 전처리 (비동기)
        # GPU: 현재 배치 추론 (동시 진행)
        results = []
        for i in range(0, len(texts), self.batch_size):
            batch = texts[i:i + self.batch_size]
            preprocessed = self.preprocess_async(batch)
            # non_blocking=True로 비동기 GPU 전송
            tensor = torch.tensor(preprocessed).cuda(non_blocking=True)
            with torch.no_grad():
                output = self.model(tensor)
            results.extend(output.cpu().tolist())
        return results

박시니어 씨가 화이트보드에 타임라인을 그렸습니다. 위쪽 줄에는 CPU, 아래쪽 줄에는 GPU라고 적었습니다.

"지금 너의 코드는 이런 식으로 동작해." 박시니어 씨가 그림을 그렸습니다. CPU가 데이터를 준비하는 동안 GPU는 빈 상자로 표시됐습니다.

GPU가 연산하는 동안 CPU는 빈 상자였습니다. 번갈아가며 쉬는 모습이었습니다.

"이상적인 모습은 이거야." 두 번째 그림에서는 CPU와 GPU 모두 빈 상자 없이 꽉 차 있었습니다. CPU가 다음 배치를 준비하는 동안 GPU는 현재 배치를 처리하고 있었습니다.

이것이 바로 파이프라이닝의 개념입니다. 공장의 조립 라인을 떠올려보세요.

자동차를 만들 때 한 대씩 완성하고 다음 차를 시작하지 않습니다. 첫 번째 차가 도색 공정에 있을 때 두 번째 차는 조립 공정에, 세 번째 차는 부품 준비 공정에 있습니다.

모든 공정이 동시에 돌아가는 것입니다. 위의 코드에서 핵심은 두 가지입니다.

첫째, ThreadPoolExecutor를 사용한 비동기 전처리입니다. preprocess_async 메서드를 보면, 여러 텍스트의 전처리를 동시에 시작합니다.

하나가 끝날 때까지 기다리지 않고, 모든 작업을 동시에 진행합니다. 둘째, non_blocking=True 옵션입니다.

cuda(non_blocking=True)를 사용하면 데이터를 GPU로 전송하면서 동시에 다른 작업을 할 수 있습니다. CPU가 다음 배치를 준비하는 동안 현재 배치가 GPU로 전송됩니다.

실제로 이 최적화를 적용하면 어떤 변화가 있을까요? 최적화 전에는 GPU 활용률이 30%였다면, 최적화 후에는 80-90%까지 올라갈 수 있습니다.

처리 속도도 2-3배 빨라집니다. 같은 하드웨어로 훨씬 더 많은 일을 할 수 있게 되는 것입니다.

추가로 pin_memory 옵션도 중요합니다. 일반적으로 CPU 메모리에 있는 데이터를 GPU로 보내려면 먼저 특별한 영역으로 복사해야 합니다.

pin_memory=True를 사용하면 이 복사 과정을 건너뛸 수 있어서 전송 속도가 빨라집니다. 김개발 씨가 코드를 수정하고 다시 nvidia-smi를 실행했습니다.

GPU 활용률이 85%를 가리키고 있었습니다. "와, 진짜 빨라졌어요!"

실전 팁

💡 - nvidia-smi --query-gpu=utilization.gpu --format=csv -l 1로 실시간 GPU 활용률을 모니터링하세요

  • DataLoader의 num_workers와 pin_memory 옵션을 활용하면 더 쉽게 구현할 수 있습니다

5. 병렬 처리 전략

김개발 씨의 회사에는 GPU가 4개 있었습니다. 지금까지는 1개만 사용했는데, 4개를 모두 활용하면 4배 빨라질까요?

박시니어 씨는 고개를 저었습니다. "단순히 4배는 아니야.

병렬 처리 전략에 따라 달라져."

여러 GPU를 활용하는 방법에는 데이터 병렬화모델 병렬화가 있습니다. 데이터 병렬화는 같은 모델을 여러 GPU에 복제하고 데이터를 나눠서 처리합니다.

모델 병렬화는 큰 모델을 여러 GPU에 나눠서 올립니다.

다음 코드를 살펴봅시다.

import torch
import torch.multiprocessing as mp
from concurrent.futures import ProcessPoolExecutor

def process_on_gpu(gpu_id, texts, model_path):
    """특정 GPU에서 배치 처리 수행"""
    # 지정된 GPU 사용
    torch.cuda.set_device(gpu_id)
    model = load_model(model_path).cuda(gpu_id)

    results = []
    for i in range(0, len(texts), 32):
        batch = texts[i:i + 32]
        with torch.no_grad():
            output = model(batch)
        results.extend(output.cpu().tolist())
    return results

def parallel_inference(texts, num_gpus=4):
    """여러 GPU에 데이터를 분산하여 병렬 처리"""
    # 데이터를 GPU 개수만큼 분할
    chunk_size = len(texts) // num_gpus
    chunks = [texts[i:i + chunk_size] for i in range(0, len(texts), chunk_size)]

    # 각 GPU에서 병렬로 처리
    with ProcessPoolExecutor(max_workers=num_gpus) as executor:
        futures = [executor.submit(process_on_gpu, i, chunks[i], "model.pt")
                   for i in range(num_gpus)]
        results = [f.result() for f in futures]

    # 결과 합치기
    return [item for sublist in results for item in sublist]

박시니어 씨가 비유를 들어 설명했습니다. "피자 가게를 생각해봐.

주문이 100개 들어왔어." 한 가지 방법은 데이터 병렬화입니다. 피자 오븐이 4개 있고, 모든 오븐이 같은 종류의 피자를 만들 수 있습니다.

주문 100개를 25개씩 나눠서 각 오븐에 할당합니다. 4개 오븐이 동시에 피자를 굽습니다.

다른 방법은 모델 병렬화입니다. 피자 하나를 만드는 과정을 분업하는 것입니다.

첫 번째 오븐에서 도우를 구우면, 두 번째 오븐에서 토핑을 올리고, 세 번째 오븐에서 치즈를 녹입니다. 하나의 큰 작업을 단계별로 나누는 것입니다.

TTS나 일반적인 추론 작업에서는 데이터 병렬화가 더 적합합니다. 모델 크기가 한 GPU에 들어가기 때문입니다.

코드를 살펴보겠습니다. parallel_inference 함수를 보면, 먼저 전체 텍스트를 GPU 개수만큼 분할합니다.

10만 개의 텍스트가 있고 GPU가 4개라면, 각 GPU가 2만 5천 개씩 담당합니다. ProcessPoolExecutor는 멀티프로세싱을 쉽게 사용할 수 있게 해줍니다.

각 프로세스는 독립적인 GPU를 사용합니다. executor.submit으로 4개의 작업을 동시에 시작하고, 모든 작업이 끝나면 결과를 합칩니다.

process_on_gpu 함수에서 중요한 부분은 torch.cuda.set_device(gpu_id)입니다. 이 줄이 없으면 모든 프로세스가 0번 GPU를 사용하려고 해서 충돌이 납니다.

각 프로세스가 자기만의 GPU를 사용하도록 명시해야 합니다. 4개 GPU를 사용하면 정확히 4배 빨라질까요?

이론적으로는 그렇지만, 현실에서는 조금 다릅니다. 데이터를 분할하고, 각 GPU에 전송하고, 결과를 모으는 데 오버헤드가 있습니다.

실제로는 3.5배에서 3.8배 정도의 속도 향상을 기대할 수 있습니다. 또한 주의할 점이 있습니다.

멀티프로세싱을 사용하면 각 프로세스가 모델을 따로 로드합니다. GPU 메모리에 같은 모델이 4개 올라가는 것입니다.

메모리가 충분한지 확인해야 합니다. 김개발 씨가 4개 GPU로 테스트해봤습니다.

단일 GPU로 2시간 걸리던 작업이 35분 만에 끝났습니다. "거의 4배네요!"

실전 팁

💡 - PyTorch의 DistributedDataParallel(DDP)을 사용하면 더 효율적인 멀티 GPU 처리가 가능합니다

  • GPU 간 통신 오버헤드를 줄이려면 NCCL 백엔드를 사용하세요

6. 실전 100개 문장 배치 생성

이론은 충분히 배웠습니다. 이제 김개발 씨는 실제로 100개의 문장을 배치로 처리하는 완전한 코드를 작성해보기로 했습니다.

지금까지 배운 모든 기법을 총동원할 때입니다.

실전에서는 메모리 관리, 에러 처리, 진행 상황 표시 등 여러 요소를 고려해야 합니다. 100개 문장 처리를 통해 배치 추론의 전체 파이프라인을 구현해봅니다.

다음 코드를 살펴봅시다.

import torch
from tqdm import tqdm

class BatchTTSProcessor:
    def __init__(self, model, batch_size=32):
        self.model = model.cuda().eval()
        self.batch_size = batch_size

    def generate(self, texts):
        """100개 문장을 효율적으로 배치 처리"""
        results = []
        total_batches = (len(texts) + self.batch_size - 1) // self.batch_size

        # 진행 상황 표시와 함께 배치 처리
        for i in tqdm(range(0, len(texts), self.batch_size),
                      total=total_batches, desc="배치 처리 중"):
            batch = texts[i:i + self.batch_size]

            try:
                with torch.no_grad(), torch.cuda.amp.autocast():
                    # 혼합 정밀도로 메모리 절약
                    outputs = self.model.synthesize(batch)
                results.extend(outputs)
            except RuntimeError as e:
                if "out of memory" in str(e):
                    # OOM 발생 시 배치를 절반으로 줄여서 재시도
                    torch.cuda.empty_cache()
                    half = len(batch) // 2
                    results.extend(self._process_small_batch(batch[:half]))
                    results.extend(self._process_small_batch(batch[half:]))

        return results

# 사용 예시
texts = ["안녕하세요, 반갑습니다."] * 100
processor = BatchTTSProcessor(model, batch_size=32)
audio_outputs = processor.generate(texts)

김개발 씨는 지금까지 배운 모든 것을 하나의 완성된 코드로 정리하고 싶었습니다. 실제 프로덕션에서 사용할 수 있는 수준의 코드 말입니다.

위의 코드는 실전에서 바로 사용할 수 있는 완전한 배치 처리 클래스입니다. 하나씩 살펴보겠습니다.

먼저 생성자에서 model.cuda().eval()을 호출합니다. cuda()는 모델을 GPU로 옮기고, eval()은 추론 모드로 전환합니다.

추론 모드에서는 드롭아웃 등이 비활성화되어 일관된 결과를 얻을 수 있습니다. generate 메서드의 핵심은 tqdm을 사용한 진행 상황 표시입니다.

100개 문장을 처리하는 데 시간이 걸리니까, 사용자가 얼마나 진행됐는지 알 수 있어야 합니다. "배치 처리 중: 50% |████████████░░░░░░░░| 2/4" 이런 식으로 표시됩니다.

**torch.cuda.amp.autocast()**는 혼합 정밀도를 활성화합니다. 보통 모델은 32비트 부동소수점을 사용하는데, 이 컨텍스트 안에서는 자동으로 16비트를 사용합니다.

메모리 사용량이 절반으로 줄어들고, 최신 GPU에서는 연산 속도도 빨라집니다. 가장 중요한 부분은 에러 처리입니다.

except 블록을 보면, OOM 에러가 발생했을 때 프로그램이 죽지 않고 복구합니다. 먼저 torch.cuda.empty_cache()로 메모리를 정리하고, 배치를 절반으로 나눠서 다시 시도합니다.

이 패턴은 실무에서 정말 중요합니다. 10만 개 중 9만 개를 처리한 시점에서 OOM이 발생하면, 처음부터 다시 시작해야 할까요?

이 코드는 그런 상황에서도 자동으로 복구하고 계속 진행합니다. 사용 예시를 보면, 100개의 문장 리스트를 만들고 processor.generate(texts)를 호출하는 것이 전부입니다.

복잡한 내부 동작은 모두 클래스 안에 캡슐화되어 있습니다. 실제로 이 코드를 실행하면 어떤 일이 일어날까요?

100개 문장이 32개씩 묶여서 4번의 배치로 처리됩니다. 마지막 배치는 4개만 있지만 괜찮습니다.

각 배치마다 GPU에서 병렬로 음성이 합성되고, 결과가 리스트에 추가됩니다. 전체 과정이 진행 바와 함께 표시됩니다.

김개발 씨가 코드를 실행했습니다. 단일 처리로 50초 걸리던 작업이 8초 만에 끝났습니다.

6배 이상의 성능 향상입니다. "이제 10만 개도 무섭지 않아요!" 김개발 씨가 자신감 있게 말했습니다.

박시니어 씨가 흐뭇하게 웃었습니다. "이제 배치 추론의 기본은 완전히 익힌 거야."

실전 팁

💡 - 프로덕션에서는 결과를 중간중간 저장하여 장애 발생 시 이어서 처리할 수 있게 하세요

  • 로그를 남겨서 나중에 성능 분석과 디버깅에 활용하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#BatchInference#GPU최적화#병렬처리#메모리관리#TTS,Optimization

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스