모델 추론 성능 최적화 - 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순위)
- 외부 API(p95 467ms) → e5-small CPU 21ms 실측(22배). CPU 상주 시 추가 비용 0, 사용자 검색어를 외부로 보내지 않음. 장애 시에는 다른 모델로 폴백하지 않고 키워드 검색으로 강등한다(모델이 다르면 저장된 벡터와 비교할 수 없음). 품질 회귀 시 되돌리기는 설정 변경이 아니라 모델 교체 절차(도서 임베딩 전량 재적재 + 취향 프로필 재생성, ERD §7)를 따른다.
- 도서 임베딩 사전 계산
- 검색 시점에 임베딩하지 않고 도서 등록·수정 시 생성해
book_embeddings저장.
- 검색 시점에 임베딩하지 않고 도서 등록·수정 시 생성해
- 인제스트 배치화
- batch_size 32~64. 실측: 5.6 → 199~231 docs/s (CPU) / ~940 (GPU). 5만 건 초기 적재 2.5시간 → 4분.
- 추천 이유는 카드 생성과 한 호출로 끝내고, 캐시를 두지 않는다
- 카드의 짧은 이유(
reason_short)와 상세용 긴 이유(reason_long)를 ③ 한 번의 LLM 호출에서 함께 만든다. AI 서버는 이 문장을 저장하지 않고 BE가 카드와 함께 받아 보관했다가 상세 페이지에서 그대로 보여준다(확정 스펙: "AI 서버는 카드를 캐시하지 않는다"). 상세 진입 시 LLM 재호출은 물론 AI 호출 자체가 0회다
- 카드의 짧은 이유(
- 처리량 확장은 프로세스 복제
- 스레드 확장이 안 되므로 uvicorn
-workers N+ 배치 서빙(마이크로배칭). 오토스케일 기준도 프로세스 단위.
- 스레드 확장이 안 되므로 uvicorn
- 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. 남은 측정 (배포 환경 확정 후)
- Gemini 하이브리드 recall — 기준선 완성 (현재 e5-small 하이브리드가 Gemini 벡터와 동급)
- int8 양자화 — Linux 서버 (개발기 Apple Silicon 은 quantized 엔진 없음)
- pgvector HNSW 실인덱스 — numpy 전수 스캔 대비 (결론 불변 예상)
- TEI/vLLM 실서빙 스택 처리량 — 현재 측정은 naive 서빙 기준 하한
- end-to-end
/search부하 테스트 (동시성 계단)