프로그래밍/AI 교과 추천 시스템 개발기

6편. 성능 최적화 - CPU에서 27배 빠르게

Tiboong 2026. 2. 20. 12:34

 

이번 편에서 다루는 것

이전 편에서 LLM을 활용해 강좌 설명을 보강했다. 하지만 심각한 성능 문제가 있었다.

1개 강좌 보강: 32초
1,200개 보강 예상: 10시간 40분

 

이번 편에서는 이 문제를 어떻게 27배 빠르게 개선했는지 다룬다.


문제 현상 분석

증상

LLM 추론이 비정상적으로 느렸다.

import time

start = time.time()
result = model.generate(**inputs, max_new_tokens=150)
elapsed = time.time() - start

print(f"추론 시간: {elapsed:.2f}초")  # 32.14초

 

비슷한 크기의 다른 모델은 1~2초대였다. 뭔가 잘못되었다.

가설 수립

느린 원인으로 생각할 수 있는 것들:

  1. 모델 크기가 큼
  2. 토큰 생성량이 많음
  3. CPU 성능 부족
  4. 데이터 타입 문제 ← 의심

병목 분석

프로파일링

Python 프로파일러로 어디서 시간이 소요되는지 확인했다.

import cProfile
import pstats

def profile_generation():
    model.generate(**inputs, max_new_tokens=150)

cProfile.run('profile_generation()', 'output.prof')

stats = pstats.Stats('output.prof')
stats.sort_stats('cumulative')
stats.print_stats(20)

발견한 것

프로파일링 결과, 이상한 함수가 많은 시간을 차지했다:

ncalls  tottime  cumtime  filename:function
 15234   18.234   18.234  {method 'to' of 'torch._C._TensorBase'}
 30468    8.123    8.123  {built-in method torch._C._nn.linear}

 

torch._C._[TensorBase.to](<http://TensorBase.to>)() 함수가 전체 시간의 50% 이상을 차지했다.

이 함수는 텐서의 데이터 타입이나 디바이스를 변환할 때 호출된다.


원인 확인: float16 문제

모델 로드 코드 확인

# 문제의 코드
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    torch_dtype=torch.float16,  # 여기가 문제!
    device_map="cpu"
)

왜 float16이 문제인가?

GPU vs CPU의 float16 지원

디바이스 float16 지원 설명
NVIDIA GPU ✅ 네이티브 Tensor Core로 최적화
Intel CPU ⚠️ 에뮬레이션 float32로 변환 후 연산
AMD CPU ⚠️ 에뮬레이션 float32로 변환 후 연산
Apple Silicon ✅ 네이티브 Neural Engine 지원

 

운영 서버는 Intel CPU였다.

에뮬레이션이란?

CPU가 float16을 직접 계산하지 못할 때, 다음 과정이 발생한다:

1. float16 데이터를 float32로 변환
2. float32로 연산 수행
3. 결과를 float16으로 다시 변환
4. (1~3을 매 연산마다 반복)

 

매 연산마다 타입 변환이 발생하면서 엄청난 오버헤드가 생긴다.


해결: float32로 변경

코드 수정

# 수정 전 (느림)
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    torch_dtype=torch.float16,  # CPU에서 느림
    device_map="cpu"
)

# 수정 후 (빠름)
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    torch_dtype=torch.float32,  # CPU 네이티브
    device_map="cpu"
)

 

단 한 줄 바꿈으로 해결되었다.

성능 비교

설정 1회 추론 시간 비고
float16 (CPU) 32.14초 에뮬레이션 오버헤드
float32 (CPU) 1.18초 네이티브 연산

 

27배 성능 향상!

전체 보강 시간

설정 1,200개 보강
float16 약 10시간 40분
float32 약 24분

 


추가 최적화

1. 배치 크기 조정

LLM은 배치 처리가 어렵지만, 임베딩은 배치로 처리할 수 있다.

# XLM-R 임베딩 배치 최적화
class EmbeddingEngine:
    def __init__(self, model_path: str, batch_size: int = 64):
        self.model = SentenceTransformer(model_path)
        self.batch_size = batch_size
    
    def encode_batch(self, texts: List[str]) -> np.ndarray:
        """최적화된 배치 임베딩"""
        return self.model.encode(
            texts,
            batch_size=self.batch_size,
            convert_to_numpy=True,
            show_progress_bar=True
        )

2. 메모리 최적화

불필요한 그래디언트 계산을 비활성화:

import torch

# 추론 시 그래디언트 비활성화
with torch.no_grad():
    outputs = model.generate(**inputs, max_new_tokens=150)

3. 토큰 생성 설정 최적화

outputs = model.generate(
    **inputs,
    max_new_tokens=150,      # 최대 토큰 수 제한
    do_sample=True,          # 샘플링 활성화 (다양성)
    temperature=0.7,         # 적절한 무작위성
    top_p=0.9,              # nucleus sampling
    repetition_penalty=1.1,  # 반복 방지
    early_stopping=True      # 조기 종료
)

성능 측정 유틸리티 구현

성능을 체계적으로 측정하기 위한 유틸리티:

import time
from contextlib import contextmanager
from typing import Optional

class PerformanceMonitor:
    def __init__(self):
        self.measurements = []
    
    @contextmanager
    def measure(self, name: str):
        """ 실행 시간 측정 컨텍스트 매니저"""
        start = time.time()
        yield
        elapsed = time.time() - start
        self.measurements.append({
            'name': name,
            'elapsed': elapsed
        })
        print(f"[{name}] {elapsed:.2f}초")
    
    def summary(self):
        """측정 요약"""
        total = sum(m['elapsed'] for m in self.measurements)
        print(f"\\n=== 성능 요약 ===")
        for m in self.measurements:
            pct = (m['elapsed'] / total) * 100
            print(f"{m['name']}: {m['elapsed']:.2f}초 ({pct:.1f}%)")
        print(f"총 시간: {total:.2f}초")

사용 예시

monitor = PerformanceMonitor()

with monitor.measure("모델 로드"):
    model = load_model()

with monitor.measure("설명 생성 x 100"):
    for course in courses[:100]:
        enhance_description(course)

monitor.summary()

 

출력:

[모델 로드] 3.21초
[설명 생성 x 100] 118.42초

=== 성능 요약 ===
모델 로드: 3.21초 (2.6%)
설명 생성 x 100: 118.42초 (97.4%)
총 시간: 121.63초

환경별 최적 설정

환경에 따라 최적의 설정이 다르다:

import torch

def get_optimal_dtype():
    """환경에 따른 최적 데이터 타입 반환"""
    
    if torch.cuda.is_available():
        # NVIDIA GPU: float16 최적
        return torch.float16
    
    elif torch.backends.mps.is_available():
        # Apple Silicon: float16 지원
        return torch.float16
    
    else:
        # CPU: float32 사용
        return torch.float32

def get_device():
    """사용 가능한 디바이스 반환"""
    
    if torch.cuda.is_available():
        return "cuda"
    elif torch.backends.mps.is_available():
        return "mps"
    else:
        return "cpu"

환경 감지 적용

class DescriptionEnhancer:
    def __init__(self, model_path: str):
        self.device = get_device()
        self.dtype = get_optimal_dtype()
        
        print(f"디바이스: {self.device}, 데이터 타입: {self.dtype}")
        
        self.model = AutoModelForCausalLM.from_pretrained(
            model_path,
            torch_dtype=self.dtype,
            device_map=self.device
        )

최종 성능 결과

최적화 전후 비교

항목 최적화 전 최적화 후 개선
1회 추론 32.14초 1.18초 27배
1,200개 보강 10시간 40분 24분 27배
메모리 사용 2.4GB 4.8GB 2배 증가

 

트레이드오프: 메모리는 2배 증가했지만, 속도는 27배 향상.

운영 서버 메모리(32GB)에서는 문제없는 수준이었다.

최종 파이프라인 성능

작업 시간
모델 로드 3초
2,000강좌 임베딩 40초
1,200강좌 설명 보강 24분
5,000학생 추천 생성 3분
전체 파이프라인 약 28분

성과 및 교훈

성과

지표 수치
성능 향상 27배
원인 float16 → float32 변경
코드 수정 1줄

배운 점

  1. 당연한 것을 의심하라: float16이 항상 빠른 것은 아니다
  2. 환경을 고려하라: GPU/CPU에 따라 최적 설정이 다름
  3. 프로파일링은 필수: 병목을 찾아야 해결할 수 있음
  4. 단순한 해결책을 먼저: 복잡한 최적화 전에 기본부터 확인

디버깅 과정 요약

1. 문제 인지: LLM 추론이 비정상적으로 느림
2. 프로파일링: to() 함수가 50% 시간 차지
3. 가설 수립: 타입 변환 오버헤드
4. 원인 확인: CPU에서 float16 에뮬레이션
5. 해결: float32로 변경
6. 검증: 27배 성능 향상 확인

다음 편 예고

모든 핵심 기능이 완성되었다. 마지막 편에서는 API 서버 구축과 배포를 다룬다.

  • FastAPI로 REST API 구축
  • API 키 인증
  • Nuitka로 단일 바이너리 빌드
  • 운영 서버 배포

시리즈 목차

  1. 프로젝트 소개 - 왜 AI 추천이 필요했나
  2. 추천을 위한 데이터 파이프라인 설계
  3. XLM-R로 강좌 임베딩 구축하기
  4. 추천 알고리즘 설계 - 가중치 기반 개인화
  5. LLM으로 강좌 설명 보강하기
  6. 성능 최적화 - CPU에서 27배 빠르게 ← 현재 글
  7. API 서버 구축과 배포