모델 추론 성능 최적화 - 100-hours-a-week/KTB4-13th-wiki GitHub Wiki

해야되는 것

0. 서비스 맥락과 결론

북적북적의 AI 추론은 3개 모델로 구성된다.

모델 용도 서빙
임베딩 검색·추천의 벡터 매칭, 도서 인제스트 (현재) 외부 API
LLM 추천 이유 생성, 대화 spec 해석 외부 API
OCR·VLM (V2) 표지 이미지 도서 인식 — OCR 우선, 실패분만 VLM 폴백 외부 API, 저빈도(이미지 턴에서만)

자체 학습 모델이 없으므로 "모델 크기로 인한 GPU 부하"는 병목이 아니다.

병목은 전부 외부 API 왕복(I/O 지연)과 LLM 토큰 생성 시간이며, 최적화는 여기에 집중한다.

결론: 임베딩을 자체 서빙(multilingual-e5-small)으로 전환하면 쿼리 임베딩 지연이 467ms → 21ms(22배), 인제스트 처리량이 5.6 → 200 docs/s(35배) 개선된다.

검색 품질은 키워드+벡터 하이브리드 기준 recall@10 0.90 으로 외부 API와 동급을 유지한다.

추천 이유는 카드 생성 시 짧은/긴 이유를 한 호출로 함께 만들어 BE가 보관하므로(AI는 캐시하지 않음),

재노출 시 AI 호출 자체가 사라져 지연이 1.3초 → 0(미호출) 이 된다.

측정 도구: bench/EXPERIMENTS.md 하니스. 로컬 개발기(Apple Silicon, Python 3.14) 기준이며 모델·백엔드 간 상대 비교로 측정함. 상세 결과: bench/results/SUMMARY.md.


1. 기존 모델 추론의 성능 지표

측정 조건: 고정 질의 30개, 예열분 제외, 3회 반복. 외부 API = Google Gemini (한국→해외 RTT 포함).

추론 경로 호출 빈도 p50 p95 자원/비용
쿼리 임베딩 (외부 API, gemini-embedding-001) 모든 검색·추천 질의 375ms 467ms 건당 과금, 호출량 최다
벡터 후보 검색 (전수 스캔, N=20만) 모든 검색·추천 6.7ms 7.5ms CPU, 무시 가능
LLM 추천 이유 생성 1건 (gemini-3.5-flash-lite, 스트리밍) 대화 턴마다 1,092ms 1,326ms 토큰 과금
└ 첫 토큰까지 (TTFB) 대화 턴마다 794ms 1,002ms —
도서 인제스트 임베딩 (외부 API 배치) 카탈로그 갱신 — ~5.6 docs/s 건별 왕복

2. 식별된 병목 요소와 원인

경로 병목 원인 근거
검색·추천 질의 쿼리 임베딩 외부 API 왕복 벡터 검색 자체는 7.5ms인데 임베딩 왕복이 467ms (약 60배). 모든 질의가 이 경로를 탄다 E1·E2 실측 대비
추천 대화 LLM 토큰 생성 시간 토큰을 순차 생성. 전체 1.3초 중 첫 토큰까지가 1.0초 E3 실측
도서 상세 "AI 추천 이유" 캐시 미스 시 LLM 재호출 동일 (도서, 조건) 조합을 매번 새로 생성 설계 분석
카탈로그 인제스트 건별 API 왕복 실시간성이 불필요한데 한 건씩 호출 (~5.6 docs/s) E1 실측
임베딩 서빙 확장 단일 프로세스가 스레드로 처리량이 안 늘어남 GIL + 추론 라이브러리 내부 스레드 경합. 동시성 1→4→8 에서 65→88→64 RPS E1 실측

정리: 과제가 든 병목 예시 중 우리에게 해당하는 것은 I/O 지연이다. DB·벡터 연산은 최적화 대상에서 제외한다(전수 스캔조차 20만 건 p95 7.5ms).


3. 적용할 최적화 기법과 선택 근거

과제가 제시한 기법 목록을 우리 병목(I/O 지연)에 대입해 선별했다.

과제 예시 기법 우리 적용 판단
자체 서빙(왕복 제거) 임베딩 자체 서빙 (e5-small) ✅ 채택 — 최다 호출 경로의 왕복·과금 동시 제거
배치 처리 인제스트 임베딩 배치화 ✅ 채택 — 실시간성 불필요, 처리량 병목
사전 연산 결과 캐싱 도서 임베딩 사전 계산. (추천 이유는 캐싱이 아니라 카드 생성 호출 한 번에 짧은/긴 이유를 함께 만들어 BE가 보관 — AI 쪽 이유 캐시 테이블은 두지 않는다, 확정 스펙 §③) ✅ 채택(임베딩) — 이유는 "캐싱" 대신 "한 호출 + BE 보관"으로 확정
Knowledge distillation 정적 임베딩(model2vec) 평가 ❌ 기각 — 100배 빠르나 recall −15~20%p (아래)
Quantization (int8) 임베딩 int8 ⏳ 배포 타깃(Linux)에서 측정 예정
하드웨어 가속(CUDA/TensorRT) GPU 서빙 + TEI/vLLM ⏳ 인제스트 대량 갱신·LLM 자체서빙 시점에

채택 기법 상세

  1. 임베딩 자체 서빙 (1순위)
    1. 외부 API(p95 467ms) → e5-small CPU 21ms 실측(22배). CPU 상주 시 추가 비용 0, 사용자 검색어를 외부로 보내지 않음. 장애 시에는 다른 모델로 폴백하지 않고 키워드 검색으로 강등한다(모델이 다르면 저장된 벡터와 비교할 수 없음). 품질 회귀 시 되돌리기는 설정 변경이 아니라 모델 교체 절차(도서 임베딩 전량 재적재 + 취향 프로필 재생성, ERD §7)를 따른다.
  2. 도서 임베딩 사전 계산
    1. 검색 시점에 임베딩하지 않고 도서 등록·수정 시 생성해 book_embeddings 저장.
  3. 인제스트 배치화
    1. batch_size 32~64. 실측: 5.6 → 199~231 docs/s (CPU) / ~940 (GPU). 5만 건 초기 적재 2.5시간 → 4분.
  4. 추천 이유는 카드 생성과 한 호출로 끝내고, 캐시를 두지 않는다
    1. 카드의 짧은 이유(reason_short)와 상세용 긴 이유(reason_long)를 ③ 한 번의 LLM 호출에서 함께 만든다. AI 서버는 이 문장을 저장하지 않고 BE가 카드와 함께 받아 보관했다가 상세 페이지에서 그대로 보여준다(확정 스펙: "AI 서버는 카드를 캐시하지 않는다"). 상세 진입 시 LLM 재호출은 물론 AI 호출 자체가 0회다
  5. 처리량 확장은 프로세스 복제
    1. 스레드 확장이 안 되므로 uvicorn -workers N + 배치 서빙(마이크로배칭). 오토스케일 기준도 프로세스 단위.
  6. LLM 응답 스트리밍 + 단계별 타임아웃(입력 5초 / 생성 20초 / 전체 30초).

서비스 맥락에서 왜 효과적인가

  • 트래픽 패턴: 커머스는 읽기 위주, 카탈로그 변경 적음 → 도서 임베딩 사전 계산이 정확히 들어맞음.
  • 호출 분포: 임베딩은 모든 질의가 타는 최다 호출 경로 → 여기 왕복을 없애는 게 체감·비용 모두에 최대 효과.
  • 비용 제약: 외부 API 예산 상한(3개월 36만원). 임베딩을 고정비(CPU 상주)로 바꾸면 트래픽 급증 시 비용이 선형 증가하지 않음.
  • 품질 민감도: 사용자에게 그대로 보이는 대화 본체 LLM 은 외부 API 유지, 품질 허용범위가 넓은 이유 문장·임베딩부터 자체화.

4. 최적화 적용 후 기대 성능 지표 (전후 비교, 동일 조건)

지표 전환 전 (실측) 전환 후 (실측) 개선
쿼리 임베딩 p95 467ms (외부 API) 21ms (e5-small CPU) 22배
검색 전체 p95 (임베딩+벡터) ~470ms ≈ 23ms 목표 300ms 통과
도서 임베딩 처리량 5.6 docs/s 199~231 docs/s (CPU) / ~940 (GPU) 35~170배
추천 이유 노출 p95 (상세 재진입) 1,326ms (매번 생성, 구 방식 가정) 0ms — AI 미호출 (BE가 보관한 문장을 그대로 표시) LLM 재호출뿐 아니라 AI 호출 자체를 제거
추천 1턴 완료 p95 — 1.5~2.7초 (파이프라인 전체) SLO 4초 통과
추천 1턴 TTFB p95 1,002ms ≤ 1.5초 (스트리밍 유지) SLO 통과
임베딩 비용 호출량 비례 종량 고정비 (CPU 상주 0원) 변동비 → 고정비

검색 품질(recall) 검증 — 임베딩 교체가 품질을 해치지 않는가

임베딩 모델 교체 시 검색 recall@10 이 기준선(Gemini) 대비 −2%p 이내여야 한다.

구성 recall@10 (국중도 서지 1,500권, 40질의)
Gemini · 벡터 단독 (기준선) 0.90 (손수 만든 60권 샘플 기준)
e5-small · 벡터 단독 0.85
e5-small · 하이브리드 (키워드 BM25 + 벡터 RRF) 0.90
model2vec(정적, distillation) · 하이브리드 0.83
  • 벡터 단독으로만 보면 e5-small 이 −10.9%p 로 게이트 미달처럼 보이나, 실제 /search 는 키워드+벡터 하이브리드 이며 "어린왕자" 같은 정확 매칭은 키워드가 잡는다.
  • 하이브리드 기준 e5-small = 0.90 으로 Gemini 벡터 단독과 동급 → 게이트 통과.
  • model2vec(distillation)은 하이브리드로도 0.83 → 본 경로 부적합. 자동완성 등 초경량 용도로만.

→ 자체 서빙 임베딩 모델 = multilingual-e5-small 로 확정.


부록 A. 측정 방법

  • 고정 입력: 질의 셋을 한 번 정하고 전·후 측정에 동일하게 사용.
  • 예열 제외: 첫 수 회는 캐시·JIT 워밍업이라 버리고 이후 측정.
  • 반복: 질의 30개 × 3회 → p50/p95/p99, 처리량(RPS).
  • recall: 질의별 정답 도서 라벨 대비 top-k 적중률. 골드셋 2종(손수 60권 / 국중도 API 1,500권).
  • 하니스: bench/bench_embed.py(임베딩 지연·처리량), bench_vector.py(벡터 검색 스케일), bench_llm.py(LLM 생성·TTFB), bench_recall.py·bench_hybrid.py(검색 품질).

부록 B. 남은 측정 (배포 환경 확정 후)

  1. Gemini 하이브리드 recall — 기준선 완성 (현재 e5-small 하이브리드가 Gemini 벡터와 동급)
  2. int8 양자화 — Linux 서버 (개발기 Apple Silicon 은 quantized 엔진 없음)
  3. pgvector HNSW 실인덱스 — numpy 전수 스캔 대비 (결론 불변 예상)
  4. TEI/vLLM 실서빙 스택 처리량 — 현재 측정은 naive 서빙 기준 하한
  5. end-to-end /search 부하 테스트 (동시성 계단)