모델 추론 성능 최적화 - 100-hours-a-week/KTB4-4th-wiki GitHub Wiki
1단계는 백엔드와 AI 서버 사이의 계약을 정했다. 상용 LLM API를 쓴다는 전제였다. 프로덕션에서는 비용 절감, 데이터 보안, 성능 최적화, 통제권 확보를 위해 GPU 서버에 모델을 직접 서빙한다. 이 문서는 v2의 GPU 서빙 최적화 방안을 정한다.
| 비용 | 한 세션은 최대 20개 대화 사이클(사용자 20회 + AI 20회, 총 40개 발화)이다. 사이클마다 응답과 추출 호출이 한 번씩이다. 여기에 요약·검색문 재작성·추천 이유 생성이 붙어 사용자당 호출 수가 많다 |
| 데이터 | 대화 원문이 외부로 나간다. 1단계는 원문을 저장하지 않기로 정했다. 저장은 안 하지만 매 턴 외부 API로 보내는 것은 그대로다 |
| 조정 여지 | 상용 API는 프롬프트 캐시, 배치, 양자화를 조정할 수 없다. 최적화 수단이 없다 |
자체 서빙으로 옮기기 전에 정해야 할 항목을 아래에 정리한다.
경로마다 최적화 목표가 다르다. "LLM 응답을 빠르게"로 묶으면 대기가 없는 구간에 지연 최적화를 하게 된다.
| 경로 | 사용자 상태 | 목표 | 주지표 |
|---|---|---|---|
| 대화 턴 (SSE) | 화면에서 대기 | 첫 글자가 빨리 나오는 것 | TTFT |
| 프로필 확정 (NU-04 → NU-04A) | 화면 전환을 기다림 | 끝나는 시각 | 완료시간 p95 |
| 리스트 생성 (NU-03 준비 중) | 대기하지 않음. 메인 화면을 씀 | 같은 시간에 더 많이 | 처리량 |
| 리스트 재생성 | 대기하지 않음 | 같은 시간에 더 많이 | 처리량 |
| 카탈로그 배치 | 오프라인 | 비용 | 시간당 비용 |
용어를 먼저 정의한다.
| 용어 | 뜻 |
|---|---|
| SSE (Server-Sent Events) | 서버가 응답을 한 번에 주지 않고 조각으로 밀어 보내는 방식. 대화 턴에서 글자가 흘러나오게 만드는 통신 방법이다 |
| TTFT (Time To First Token) | 요청부터 첫 토큰이 나올 때까지의 지연 |
| 잡 (job) | 요청 처리와 분리해 뒤에서 돌리는 일감 한 건. 사용자 승인 이후의 검색문 재작성·임베딩·리스트 생성이 여기 해당한다. 호출자가 결과를 기다리지 않고 202 + jobId만 받아간다는 뜻에서 비동기다 |
| 잡 큐 · 워커 | 잡을 쌓아두는 대기열이 잡 큐이고, 거기서 잡을 꺼내 실행하는 프로세스가 워커다. 잡은 태어났다 끝나는 일감이고 워커는 계속 살아 있으면서 잡을 소화한다 |
| 카탈로그 | 추천 대상 상품과 그 벡터를 담은 저장소. AI 서버가 소유하는 유일한 데이터다 |
| 카탈로그 배치 | 카탈로그를 채우고 갱신하는 오프라인 작업. 크롤링, 속성 추출, 상품 임베딩, 가격·재고 갱신이 스케줄로 돈다. 사용자 요청과 무관하게 도므로 대기하는 사람이 없다 |
사용자가 AI 서버를 기다리는 화면은 대화와 분석 둘뿐이다(모델 API 설계의 「사용자 대기가 발생하는 구간」). 최적화 대상은 토큰 생성 속도와 분석 구간 소요시간이다.
1. 추천 이유 생성 — 다이어그램과 설계 이유
리스트는 한 모드당 20개다. self와 gift를 합쳐 최대 40개다. 이유를 상품마다 생성하면 LLM 호출이 40회다. 이 40회가 전부 하나의 리스트 생성 잡 안에서 돈다.
리스트 사전 계산으로 이유 생성이 화면 진입 경로에서 빠졌다. 생성 자체는 남아 있고 추출 경로로 옮겨졌다.
flowchart LR
subgraph AS["현재 — 상품 단위"]
direction TB
A1[드리퍼] --> A2[LLM 호출]
B1[드립포트] --> B2[LLM 호출]
C1[핸드밀] --> C2[LLM 호출]
D1[...20개] --> D2[LLM 호출]
A2 --> AR["40개의 서로 다른 문장"]
B2 --> AR
C2 --> AR
D2 --> AR
end
subgraph BS["변경 — 검색 쿼리 단위"]
direction TB
Q1["쿼리 · 캠핑장에서 핸드드립"] --> QL[LLM 호출 1회]
QL --> QR["문장 1개"]
QR --> P1[드리퍼]
QR --> P2[드립포트]
Q2["쿼리 · 손목 부담 적은 분쇄"] --> QL2[LLM 호출 1회]
QL2 --> QR2["문장 1개"]
QR2 --> P3[핸드밀]
end
- 문제는 호출 수가 아니라 생성 단위다. 상품은 취향 항목에서 만든 검색 쿼리로 찾는다. 같은 쿼리에 비슷한 유사도로 걸린 상품은 같은 근거로 뽑힌 것이다.
- "캠핑에서 커피 내려 드시는 걸 좋아해서"는 드리퍼와 드립포트에 모두 해당한다. 상품마다 문장을 새로 쓰면 근거는 같고 표현만 달라진다.
-
items[].reason은 필드와 타입이 그대로다. 계약은 바뀌지 않고 생성 단위만 바뀐다. -
gift모드는 원문 인용을 금지한다. 지금은 프롬프트 입력 차단, 후검증, 실패 시 재생성으로 막는다. 생성 단위가 쿼리로 올라가면 검사 대상이 40개에서 6개 안팎으로 준다. - 한 쿼리에 상품이 몰리면 같은 문장이 반복된다. 다양성 선별이 가격대와 카테고리로 분산시키므로 쏠림은 크지 않다. 반복이 보이면 쿼리 안에서 유사도 대역(상·중)으로 한 번 더 쪼갠다.
2. 사용자 대기 구간의 분할 — 다이어그램과 설계 이유
1단계는 대화 종료 후의 처리를 두 구간으로 나눴다. 앞 구간은 프로필을 확정하고 요약문과 표시 키워드를 만들어 확인 화면에 보여준다. 사용자가 승인하면 뒤 구간이 리스트를 만든다.
flowchart TD
C[프로필 확정] --> SUM[요약문 생성<br/>응답 모델]
SUM --> K[표시 키워드 선별]
K --> UI["확인 화면<br/>사용자 대기"]
UI --> R[검색 문장 재작성<br/>파생 쿼리 생성]
R --> E[임베딩 배치]
E --> V[벡터 검색 · 하드필터]
V --> SC[스코어 · 다양성 선별]
SC --> RE[추천 이유 생성<br/>쿼리 단위]
RE --> D["self + gift 반환"]
style UI fill:#fff4e6
style SUM fill:#f0f7ff
style RE fill:#f0f7ff
- 앞 구간은 모델 호출이 요약문 하나뿐이라 지연 편차가 작다. 동기 응답으로 처리하고 타임아웃을 5초로 짧게 둔다. 실패하면 요약 없이 프로필과 키워드만 반환한다.
- 뒤 구간은 사용자가 메인 화면으로 이동한 뒤에 돈다. 화면 앞에서 완료를 기다리지 않으므로 지연 최적화 대상이 아니다. 목표는 처리량이다.
- 요약문 생성과 이유 생성이 다른 구간으로 갈라져 같은 GPU를 두고 경쟁하지 않는다. 이전 설계에서 두 호출을 병렬로 두려던 계획은 필요가 없어졌고, continuous batching이 있어야 이득이 난다는 조건도 함께 없어졌다.
- 부분 실패를 전체 실패로 만들지 않는다. 요약이 실패해도 프로필은 반환하고, 이유 생성이 실패해도 리스트는 반환한다.
3. 대화 턴의 프리필과 KV 캐시 — 다이어그램과 설계 이유
모델은 요청 사이에 아무것도 기억하지 않는다. 지난 대화를 알게 하려면 매 턴 시스템 프롬프트와 누적 이력을 통째로 다시 보내야 하고, 프롬프트 길이는 턴이 갈수록 늘어난다. 받은 프롬프트를 전부 읽는 계산이 프리필이고, 첫 토큰이 나오기까지의 지연(TTFT)이 여기서 결정된다.
KV 캐시(Key-Value cache)는 이미 계산한 어텐션 키·값을 다음 요청에서 다시 쓰는 장치다. 앞에서부터 직전 요청과 완전히 같은 구간까지만 재사용된다. 어긋나는 지점이 나오면 거기서부터 끝까지 다시 계산한다. 어떤 위치의 계산 결과에는 그 앞의 모든 토큰이 반영되어 있으므로, 앞이 하나 바뀌면 뒤의 값이 전부 달라지기 때문이다.
프롬프트 조립 순서는 1단계에서 정하지 않았다. 상용 API에서는 조정할 수 없는 값이었다. 자체 서빙에서는 이 순서가 매 턴 프리필 비용을 좌우한다.
[시스템(불변)] → [누적 이력] → [이번 턴 지시 + 발화]로 고정한다.
flowchart LR
S["시스템 프롬프트<br/>모든 턴에 동일"] --> H["누적 이력<br/>1턴부터 직전 턴까지<br/>뒤에만 붙는다"]
H --> N["이번 턴 지시문 + 발화<br/>매 턴 새로 생긴다"]
S -. 캐시 재사용 .-> C[("KV 캐시")]
H -. 캐시 재사용 .-> C
N --> P["신규 프리필<br/>= 이번 턴 토큰만"]
style C fill:#fff4e6
style P fill:#f0f7ff
턴이 진행돼도 새로 계산하는 양은 이번 턴 발화뿐이다.
5턴 [시스템][1~4턴 이력] + [5턴 지시+발화]
6번째 사이클 [시스템][1~5 사이클 이력] + [6번째 지시+발화]
20번째 사이클 [시스템][1~19 사이클 이력] + [20번째 지시+발화]
└──── 캐시 재사용 ────┘ └─ 신규 프리필 ─┘
| 규칙 | 이유 |
|---|---|
| 이력을 자르지 않는다. 최대 20개 대화 사이클을 전량 누적한다 | 오래된 사이클을 창 밖으로 밀어내면 그 지점부터 접두사가 어긋나 매번 이력 전체를 다시 계산한다. 상한이 20개 사이클이라 전량 누적해도 프롬프트가 크지 않고, 줄어드는 입력량보다 재계산 비용이 크다 |
| 매 턴 바뀌는 지시문을 맨 뒤에 둔다 | 시스템 프롬프트나 이력 앞에 붙이면 첫 블록부터 무효가 되어 재사용 구간이 남지 않는다 |
| 시스템 프롬프트를 세션마다 바꾸지 않는다 | 사용자별로 앞부분이 갈리면 서로 다른 세션 사이의 재사용까지 사라진다 |
| 조립을 템플릿 엔진에 맡기지 않는다 | 공백이나 구분자가 한 글자만 달라져도 접두사가 어긋난다 |
캐시는 계산을 수행한 인스턴스의 GPU 메모리에 있다. 다른 인스턴스는 그 값을 볼 수 없다. 인스턴스를 2대 이상 두면 같은 세션의 턴이 매번 다른 인스턴스로 흩어져 위 이득이 전부 사라진다.
sessionId 해시로 인스턴스를 정해 같은 세션을 항상 같은 인스턴스로 보낸다. 세션 고정 라우팅이고, 애플리케이션 코드가 아니라 로드밸런서 설정이므로 클라우드 담당과 합의한다(O4). 인스턴스를 늘리기 전에 확정해야 하며, 나중에 바꾸면 이 절의 최적화가 무효가 된다.
E2에서 서빙 단의 prefix caching(접두사 캐싱)을 켜고 끈 두 조건을 같은 12턴 스크립트로 비교한다. 끄려면 서버 재기동이 필요하다. 이 값이 없으면 조립 순서의 효과와 캐시의 효과를 구분할 수 없다.
콜드와 웜을 분리해서 잰다. 조립 순서의 효과는 웜에서만 나타난다.
4. 임베딩 호출 — 설계 이유
query 역할 항목은 프로필당 5~12건이다. 한 건씩 부르면 왕복 지연이 항목 수만큼 쌓인다.
-
query항목 전체를 한 번에 배치로 호출한다. 5~12회가 1회가 된다. - 정규화한 취향 표현을 키로 캐시한다. "캠핑", "커피" 같은 표현은 사용자 간 반복률이 높다.
- 임베딩 모델은 상품 벡터와 같은 벡터 공간이어야 한다. 바꾸면 상품 벡터를 전부 다시 계산해야 한다.
- 임베딩은 자체 서빙하지 않고 Upstage Embed 2(
solar-embedding-2, 1024차원) API를 쓴다. 자체 서빙으로 옮긴 근거 셋이 임베딩에는 걸리지 않는다. 호출이 프로필당 배치 1회라 사용자당 40회인 대화와 자릿수가 다르고, 나가는 것은 대화 원문이 아니라 재작성된 파생 쿼리이며, 배치와 캐시는 이 문서의 O2가 직접 다룬다. GPU에 올리지 않는 만큼이 KV 캐시 몫으로 간다. - API는 한 요청에 문자열 100개까지 받는다.
query항목이 프로필당 5~12건이므로 배치 상한에 걸리지 않는다.
5. 최적화 대상이 아닌 것
pgvector 인덱스를 병목 후보로 검토했다. 규모를 계산하면 대상이 아니다.
상품 1만 개 × 벡터 3종 = 3만 벡터
1024차원 float32 → 약 120MB
이 크기는 인덱스 없이 전수 계산해도 수십 ms다. 상품 100만 개에서 다시 본다.
/health는 외부 모델을 호출하지 않는다. 피드백은 짧은 동기 응답이다. 둘 다 사용자 대기 경로가 아니다.
POST /v1/recommendations는 백엔드나 배치가 부른다. 수 초가 걸려도 화면 체감에 걸리지 않는다. 지연이 아니라 처리량으로 본다.
1단계는 계약을 고정했고 서빙 방식은 정하지 않았다. 이 단계에서 정할 항목은 다음과 같다.
| 1단계에서 정해진 것 (건드리지 않는다) | 2단계에서 정하는 것 | |
|---|---|---|
| 대화 턴 | 한 턴은 응답 호출 + 추출 호출 두 번, SSE 스트리밍 | 두 호출에 어떤 크기의 모델을 줄지, 프롬프트를 어떤 순서로 조립할지 |
| 델타 추출 |
json_schema(strict) 형식 강제 |
그 강제를 서빙 스택의 guided decoding으로 처리할지 |
| 프로필 확정 |
close가 동기로 프로필·요약문·키워드를 반환 |
요약문 생성 타임아웃을 어디에 둘지 |
| 리스트 생성 |
202 + jobId + 폴링, 사용자 대기 경로가 아님 |
같은 GPU로 잡을 몇 개까지 소화할지 |
| 추천 이유 |
items[].reason 필드와 타입 |
그 문장을 몇 번의 호출로 만들지 |
| 인프라 | 프라이빗 서브넷, SSE 지원 로드밸런서 | GPU 스펙, 서빙 스택, 라우팅 규칙 |
계약은 바뀌지 않는다. 아래 최적화를 전부 적용해도 백엔드가 부르는 엔드포인트와 스키마는 그대로다. 예외는 마지막 절에 정리한다.
| 경로 | 목표 | 근거 |
|---|---|---|
| 대화 턴 TTFT | p95 1초 이내 | 첫 글자가 늦으면 사용자가 응답 없음으로 판단한다 |
| 프로필 확정 | p95 3초 이내 | 대화 종료에서 확인 화면까지의 전환 시간이다. 모델 호출이 요약문 하나뿐이다 |
| 리스트 생성 잡 | 목표값 미확정 | 사용자가 메인 화면에서 다른 것을 하는 동안 돈다. 측정 후 정한다 |
| 동시 세션 | 8세션에서 TTFT p95가 1세션 대비 2배 이내 | 대화가 몰리는 시간대의 최소 기준 |
| 병목 | 원인 | 처방 | |
|---|---|---|---|
| ① | 추천 이유 생성이 한 잡에서 40회 | 생성 단위가 상품이다 | O1 |
| ② | 임베딩을 항목마다 부른다 | 5~12회 왕복 누적 | O2 |
| ③ | 확정 구간이 요약문 생성에 묶인다 | 동기 응답 안에서 모델을 부른다 | O3 |
| ④ | 캐시 히트가 로드밸런서에 걸린다 | 세션이 인스턴스에 고정되지 않는다 | O4 |
| ⑤ | 턴마다 시스템 프롬프트를 다시 프리필한다 | 조립 순서가 캐시 조건을 만족하지 않으면 매 턴 전량 재계산 | O8 |
| ⑥ | 응답 모델 원본이 GPU 1장에 들어가지 않는다 | bf16 61GB | O9 |
| 현재 | 변경안 | |
|---|---|---|
| 생성 단위 | 상품 | 검색 쿼리(= 취향 항목) |
| LLM 호출 수 | 20 × 2모드 = 40회 | 쿼리 6 × 2모드 = 12회, 또는 모드당 배치 1회 = 2회 |
items[].reason |
상품마다 다른 문장 | 같은 쿼리에 걸린 상품이 공유 |
| 계약 변경 | — | 없음 |
쿼리 단위와 배치 중 무엇을 쓸지는 E6에서 정한다. 배치는 호출이 2회로 준다. 대신 한 번의 실패가 리스트 전체 문구를 날린다. 부분 실패 격리와 호출 수를 맞바꾸는 선택이다.
query 항목 전체를 한 번에 넘긴다. 정규화한 표현을 키로 벡터를 캐시한다.
1단계가 확정과 리스트 생성을 나누면서 요약문 생성이 사용자 대기 경로로 들어왔다. 대화 종료에서 확인 화면까지의 전환이 이 호출 하나에 묶인다.
대화 턴의 타임아웃 20초를 그대로 쓰면 최악의 경우 사용자가 20초 동안 빈 화면을 본다. 타임아웃을 5초로 두고 재시도하지 않는다. 실패하면 summary를 null로 반환하고 프로필과 키워드만 보여준다. 요약문은 친구가 읽는 글이라 없어도 확인과 리스트 생성이 진행된다.
이전 설계에서는 요약문 생성을 검색 갈래와 병렬로 돌려 6초 예산을 맞추려 했다. 구간이 나뉘면서 두 호출이 다른 시점에 돌게 되어 병렬화가 필요 없어졌고, continuous batching이 있어야 이득이 난다는 조건도 함께 사라졌다.
같은 sessionId는 같은 인스턴스로 보낸다. 해시 기반으로 처리한다. 클라우드 구성을 정하기 전에 확정해야 한다. 나중에 바꾸면 O8의 이득이 사라진다.
| 역할 | 모델 | 고른 이유 |
|---|---|---|
| 응답·요약·검색문 재작성·이유 |
kakaocorp/kanana-1.5-8b-instruct-2505 (dense 8B, BF16) |
로컬 대화 품질을 확인했고 24GB GPU 한 장에 양자화 없이 올릴 수 있다. 검색문 재작성도 문맥과 상품 사용 맥락을 이해해야 하므로 이 모델을 쓴다 |
| 턴 델타 추출 |
kakaocorp/kanana-2-1.3b-instruct (BF16) |
[STATE] + 직전 AI + 현재 사용자에서 고정 스키마 델타만 만들고 서버가 검증·병합한다. 골드셋 필수재현율을 통과할 때 채택한다 |
여기서의 경량화는 역할별로 필요한 언어 이해 범위를 나누는 방식이다. 자연스러운 대화, 공개용 요약, 검색 의도 재작성에는 8B를 쓰고, 형식이 강제된 턴별 변경분에는 1.3B를 쓴다. 한 모델에 둘 다 시키면 모든 요청을 8B 비용으로 처리하게 된다.
프롬프트 조립 순서는 prefix caching에 의존한다. 델타 스키마 강제는 guided decoding(스키마에 맞는 토큰만 생성하도록 제한하는 디코딩)에 의존한다. 둘이 없는 스택은 후보가 아니다. vLLM을 기본으로 두고 측정한다.
| 층 | 무엇 | 출처 |
|---|---|---|
| L0 | prefix KV 캐시 (시스템 프롬프트 · 대화 이력) | O8 + O4로 보장 |
| L1 | 임베딩 캐시 (정규화 취향 표현 → 벡터) | O2 |
| L2 | 리스트 사전 계산 | 1단계 계약 |
| L3 | 상품 파생값 배치 컬럼 | 1단계 계약 |
| L4 | 가격·재고 배치 갱신 | 1단계 계약 |
L1은 인스턴스 밖에 둔다. 프로세스 메모리에 두면 캐시가 인스턴스마다 따로 쌓인다. 임베딩 캐시는 "캠핑", "커피" 같은 표현이 사용자 사이에서 반복된다는 전제로 얻는 이득이라, 저장 위치가 나뉘면 히트율이 인스턴스 수만큼 나뉜다. O4의 세션 고정 라우팅도 여기서는 도움이 되지 않는다. 그 라우팅은 같은 세션을 같은 곳으로 보내는 규칙이고, L1이 노리는 것은 서로 다른 사용자 사이의 공유다.
리스트 생성 잡의 실행 위치도 이유가 된다. 임베딩 호출은 이 잡 안에서 일어난다. 잡을 별도 워커가 돌리면 API 프로세스 메모리에 둔 캐시는 워커에서 보이지 않는다. 공용 저장소여야 두 쪽이 같은 캐시를 본다.
이미 세션 상태와 잡 큐로 Redis를 쓰고 있어 새로 붙이는 인프라가 아니다. TTL과 용량 상한(LRU 축출)을 저장소가 처리하므로 만료 관리를 직접 짤 필요도 없다. 크기는 1024차원 float32 기준 벡터 하나가 약 4KB다. 표현 10만 개를 담아도 수백 MB 수준이라 한 대로 감당한다.
L0은 같은 방식으로 옮길 수 없다. KV 캐시는 GPU 메모리에 있고, 직렬화해서 꺼내 옮기면 다시 계산하는 것보다 비싸다. 그래서 L0은 저장소 공유가 아니라 O4의 라우팅으로 푼다. 두 층은 같은 캐시라도 해법이 다르다.
[시스템(불변)] → [누적 이력] → [이번 사이클 지시 + 발화] 순서를 쓴다. 이력은 최대 20개 대화 사이클 전량 누적하고 잘라내지 않는다. 캐시된 접두사에 신규 토큰만 얹으면 대화가 진행돼도 매번 프리필하는 양이 이번 발화 중심으로 유지된다. 상세는 프리뷰 3에 있다.
상용 API에서는 정할 수 없는 항목이다. 자체 서빙이라서 조정할 수 있다.
응답 모델 공식 BF16 체크포인트는 약 16.1GB라 24GB GPU 한 장에 적재할 수 있다. 남은 VRAM은 CUDA graph, activation, KV cache가 사용하므로 응답 모델은 단독 worker로 두고 컨텍스트를 8K로 제한한다. 초기 운영 기준은 공식 BF16이며, 양자화는 24GB 적재가 목적이 아니라 더 높은 동시성이나 비용 절감이 실측으로 필요한 경우에만 별도 품질 실험으로 검토한다.
| 환경 | 무엇을 | 왜 거기서 |
|---|---|---|
| RunPod 24GB 응답 worker·16GB 추출 worker + vLLM | 모델 추론 자체 (E1~E7) | 실제 운영 GPU에서 prefix caching · guided decoding · continuous batching과 청구초를 함께 측정해야 한다 |
| 로컬 + FastAPI | API 계약 경로 (SSE 릴레이, 폴링, 잡 큐) | 모델이 아니라 경로를 재므로 GPU가 필요 없다 |
eval/ 골드셋 |
품질 회귀 | 지연만 보면 품질이 미달인 구성을 고른다 |
응답 모델은 O9의 공식 BF16 체크포인트로 올린다. GPU 종류, 모델 revision, vLLM image digest를 결과에 함께 적는다.
같은 조건이 아니면 전후 비교가 성립하지 않는다.
- 발화는 고정 12개 대화 사이클 스크립트를 쓴다. 유저 시뮬레이터는 입력 토큰 수가 매번 달라져 비교가 깨진다. 시뮬레이터는 품질 평가에만 쓴다
- E8은 12개 사이클 스크립트를 마친 뒤 확정된 프로필을 주입하고 정정 대화를 3개 사이클 잇는다. 이 경로는 캐시가 비어 있는 상태에서 누적 이력을 한 번에 프리필하므로 다른 실험과 조건이 다르다
- 온도(응답 0.7 · 추출 0.1), 대화 사이클 상한, 컨텍스트 길이,
limit을 고정한다 - 콜드와 웜을 분리해서 잰다. 프롬프트 조립 순서의 효과는 웜에서만 나타난다
- 각 조건을 N회 반복하고 p50과 p95를 낸다
| 실험 | 답하는 질문 | 제출 항목 | |
|---|---|---|---|
| E1 | 베이스라인 — 턴별 TTFT · 완료시간 · tok/s | 출발점은 어디인가 | 기존 성능 지표 |
| E2 | 고정 조립 순서 × prefix caching on/off | 캐시 재사용이 프리필을 얼마나 줄이는가 | 병목 분석 · 개선 수치 |
| E3 | 호출 분리 vs 단일 호출 | 1단계가 정한 두 호출 구조가 자체 서빙에서도 유리한가 | 병목 분석 · 개선 수치 |
| E4 | 구조화 출력 on/off | 형식 준수가 모델 문제인가 서빙 스택 문제인가 | 최적화 기법 근거 |
| E5 | 추출 1.3B vs 응답 8B | 추출을 작은 모델로 내려도 필수 신호를 유지하는가 | 최적화 기법 근거 |
| E6 | 이유 생성 단위 3종 | O1이 호출 수를 얼마나 줄이는가 | 최적화 후 지표 |
| E7 | 동시 세션 1/2/4/8 | 몇 명까지 받을 수 있는가 | 최적화 후 지표 |
| E8 | 정정 대화 첫 턴 (프로필 주입, 캐시 콜드) | 이어가기 첫 턴이 TTFT 최악 케이스인가 | 최적화 후 지표 |
측정 코드는 modeltest.ipynb에 있다. 마지막 셀이 아래 결과 표를 출력한다.
경량화는 품질을 깎는다. 허용 범위를 미리 정하고 그 안에서만 채택한다.
필수재현율 ≥ 베이스라인 − 5%p
금지위반 증가 0
기존 평가 리포트의 숫자는 구 단일 호출 프롬프트와 다른 모델 기준이다. 지금 구조의 베이스라인이 아니다. 카나나 두 모델로 골드셋을 다시 돌린 값을 베이스라인으로 삼는다.
- 벡터 검색과 임베딩 모델은 재지 않는다. 카탈로그와 pgvector가 아직 없다. 이유 생성 실험의 후보 상품은 합성이며 호출 수와 소요시간만 유효하다
- 절대값으로 SLO(Service Level Objective)를 판정하지 않는다. A100 1장, 4bit, vLLM 기준이다. 서빙 스펙이 바뀌면 값도 바뀐다. 구성 간 상대 비교만 근거로 쓴다
측정하지 못한 구간은 "미측정"으로 표기한다. 임의 숫자로 채우지 않는다.
측정 후 채운다.
modeltest.ipynb의 출력이 이 표로 들어온다.
| 구간 | 지표 | 베이스라인 | 최적화 후 |
|---|---|---|---|
| 대화 턴 | TTFT p50 / p95 | ||
| 대화 턴 | 턴 완료 p95 | ||
| 대화 턴 | 출력 tok/s | ||
| 추출 | 호출 지연 p95 | ||
| 추출 | 형식 위반률 | ||
| 이유 생성 | LLM 호출 수 | 40 | |
| 이유 생성 | 총 소요 | ||
| 동시성 | 8세션 TTFT p95 | ||
| 정정 대화 | 첫 턴 TTFT p95 | ||
| 자원 | GPU 메모리 | ||
| 품질 | 필수재현율 / 금지위반 |
| 항목 | 변경 | 영향 범위 |
|---|---|---|
limit 기본값 40 → 20 |
한 모드당 20개, self+gift 합쳐 최대 40개 |
백엔드 저장 개수. 반영 완료 |
items[].reason 생성 단위 |
상품 단위 → 검색 쿼리 단위 | 필드와 타입은 그대로. AI 서버 내부. 반영 완료 |
| 요약문 생성 타임아웃 5초 | 확정 응답이 요약 없이 나갈 수 있다 | 백엔드·프론트. summary가 null인 경우를 처리해야 한다 |
| 세션 고정 라우팅 |
sessionId 해시 기반 |
인프라. 클라우드 담당과 합의 필요 |
프론트엔드에 가는 영향은 없다. 백엔드는 저장 개수만 확인한다. 클라우드는 라우팅 규칙만 확인한다.
이 문서는 무엇을 어떻게 서빙할지를 정한다. 어떤 단위로 묶어 배포할지는 정하지 않는다.
| 넘기는 것 | 왜 여기서 정하지 않는가 | 어디서 |
|---|---|---|
| 세션 고정 라우팅을 성립시킬 배포 구조 | O4는 같은 세션을 같은 인스턴스로 보내라는 요구조건이다. 어떤 배포 단위로 만족시킬지는 구조 문제다 | 3단계 |
| 모델 서버와 앱의 분리 | 배포 주기가 다르다는 사실은 확인했다. 경계를 정하는 것은 모듈화 작업이다 | 3단계 |
| 리스트 생성 잡의 실행 위치 | 잡 실행 주체를 정하려면 모듈 구조가 먼저다 | 3단계 |
| 인스턴스 수, 오토스케일링, 대시보드 | 스펙과 구조가 정해진 뒤의 운영 설계다 | 7단계 |
| 항목 | 확정 시점 |
|---|---|
| 임베딩 API 장애 시 처리 | 잡을 실패시킬지, 캐시된 벡터만으로 축소 검색할지. 카탈로그 적재 후 히트율을 보고 |
| 이유 생성 방식 (쿼리 단위 vs 배치) | E6 결과 |
| 추출 모델 크기 확정 | E5 + 골드셋 재측정 |
| 리스트 생성 잡의 완료시간 목표 | E1·E6 결과 |
| 정정 대화 경로의 TTFT | E8 결과 |
| 서빙 인스턴스 스펙 | E1·E7 결과와 예상 동시 세션 수 |