16장. vLLM 서빙 벤치마킹과 RAG 최적화
캐시
단골 식당은 자주 나가는 메뉴의 육수를 미리 끓여 두고, 같은 손님이 늘 시키는 메뉴는 묻지 않고 기억합니다. 같은 일을 다시 하지 않는 것이 가장 확실한 속도 올리기입니다. RAG 에도 미리 해 두거나 기억해 둘 수 있는 일이 여럿 있습니다.
그림의 네 가지는 왼쪽부터 무거운 것을 미리 하는 순서입니다. 미리 만든 색인은 문서 임베딩을 한 번만 합니다. 질문 임베딩 캐시는 같은 질문의 벡터를 기억합니다. 답 캐시는 같은 질문에 지난 답을 그대로 돌려줍니다. 프리픽스 캐시는 앞부분이 같은 프롬프트의 KV 캐시를 엔진이 다시 씁니다. 미리 끓인 육수가 색인, 단골 메뉴 기억이 답 캐시입니다.
캐시 견주기
| 캐시 | 줄이는 단계 | 맞히는 조건 | 조심할 것 |
|---|---|---|---|
| 미리 만든 색인 | 색인 | 늘 | 문서가 바뀌면 다시 만든다(11장) |
| 질문 임베딩 | 검색 | 같은 질문 | 효과가 작다 |
| 답 캐시 | 검색부터 생성까지 모두 | 같은(또는 같다고 본) 질문 | 문서가 바뀌면 지운다, 사용자마다 다른 답은 넣지 않는다 |
| 프리픽스 캐시 | 프리필 | 앞부분이 같은 프롬프트 | 바뀌는 부분을 프롬프트 뒤쪽에 둔다 |
질문을 다듬어 캐시 맞히기
같은 질문도 띄어쓰기나 물음표가 다르면 글자로는 다른 질문입니다. 캐시의 열쇠를 만들기 전에 질문을 다듬습니다.
import re
def normalize(q):
"""빈칸을 하나로 줄이고 앞뒤 빈칸과 끝의 물음표·마침표를 지운다"""
return re.sub(r"\s+", " ", q).strip().rstrip("?.").strip()
cache = {}
for q in ["연차는 며칠인가요?", "연차는 며칠인가요", " 연차는 며칠인가요? ", "반차는 몇 시간인가요?"]:
key = normalize(q)
hit = key in cache
cache.setdefault(key, f"(답: {key})")
print(f"{q!r:20s} → 열쇠 {key!r} · {'캐시에서' if hit else '새로 만듦'}")코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
re.sub(r"\s+", " ", q) | re 는 regular expression(정규 표현식, 글자 꼴을 찾는 규칙)의 줄임말이고 sub 는 substitute(바꾸다)다. 빈칸이 하나 이상 이어진 곳(\s+)을 빈칸 하나로 바꾼다 |
.strip() | strip(벗기다)이다. 앞뒤 빈칸을 지운다 |
.rstrip("?.") | r 은 right(오른쪽)다. 오른쪽 끝의 물음표와 마침표를 지운다 |
key in cache | 다듬은 질문이 이미 캐시에 있는지 본다. 있으면 캐시를 맞힌(hit) 것이다 |
cache.setdefault(key, 값) | 열쇠가 없을 때만 값을 넣는다. 이미 있으면 그대로 둔다 |
{q!r:20s} | !r 은 repr(representation, 있는 그대로의 모습)로 따옴표와 빈칸까지 보이게 찍는다. :20s 는 20칸 너비로 맞춘다 |
이 코드가 뜻하는 것
단골 식당 직원은 "김치찌개 하나요", "김치찌개 하나요?", "김치찌개 하나"를 모두 같은 주문으로 알아듣습니다. 말투가 조금 달라도 같은 주문이면 미리 해 둔 것을 내줍니다. 여기서 손님의 말이 질문, 직원이 알아들은 주문이 캐시의 열쇠입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
normalize(q) | 빈칸과 끝의 물음표를 다듬는다 | 손님의 말을 주문표의 메뉴 이름으로 바꾼다 |
hit = key in cache | 전에 본 주문인지 본다 | 기억하는 주문인가 |
cache.setdefault(...) | 처음 본 주문이면 답을 기억해 둔다 | 장부에 적는다 |
| 첫 질문 | 새로 만듦 | 처음 들은 주문 |
| 둘째, 셋째 질문 | 캐시에서 | 빈칸과 물음표만 다른 같은 주문 |
| 넷째 질문 | 새로 만듦 | 반차는 다른 메뉴다 |
질문 넷 가운데 둘을 캐시가 받아 냈습니다. 다듬지 않았다면 네 질문의 글자가 모두 달라 하나도 맞히지 못했을 것입니다. 비유와 다른 점은, 이 다듬기는 뜻이 같은 다른 말("연차 일수")까지 알아듣지는 못한다는 것입니다.
작게 줄여서 눈으로 보기
다듬기 세 단계를 한 줄씩 나눠 찍고, setdefault 가 어떻게 도는지 봅니다. 질문은 설명용입니다.
import re
q = " 연차는 며칠인가요? "
step1 = re.sub(r"\s+", " ", q) # 빈칸 여러 개를 하나로
step2 = step1.strip() # 앞뒤 빈칸 지우기
step3 = step2.rstrip("?.") # 끝의 물음표·마침표 지우기
print(repr(step1))
print(repr(step2))
print(repr(step3))
cache = {}
print(cache.setdefault("연차", "답1")) # 없으니 넣고 넣은 값을 돌려준다
print(cache.setdefault("연차", "답2")) # 이미 있으니 원래 값을 돌려준다' 연차는 며칠인가요? '
'연차는 며칠인가요?'
'연차는 며칠인가요'
답1
답1| 결과 | 읽는 법 |
|---|---|
' 연차는 며칠인가요? ' | 이어진 빈칸이 모두 하나로 줄었다. 앞뒤에 하나씩 남았다 |
'연차는 며칠인가요?' | strip() 이 앞뒤 빈칸을 지웠다 |
'연차는 며칠인가요' | rstrip("?.") 이 끝의 물음표를 지웠다. 이것이 캐시의 열쇠다 |
답1, 답1 | 처음에는 넣은 값을 돌려주고, 두 번째는 이미 있는 답1 을 그대로 돌려준다. 답2 는 들어가지 않았다 |
주의할 점
- 답 캐시는 문서가 바뀌면 틀린 답을 돌려줍니다. 문서를 고칠 때 캐시를 비우거나, 캐시에 만든 시각을 적어 일정 시간이 지나면 버립니다.
- 사용자의 등급(11장)에 따라 볼 수 있는 문서가 다르면, 등급을 열쇠에 함께 넣어 다른 등급의 답이 새어 나가지 않게 합니다.
- 뜻이 같은 다른 말("집에서 일하는 날", "재택 근무 일수")까지 맞히려면 질문 임베딩이 가까운지로 찾는 의미 캐시를 씁니다. 가까움의 기준을 너무 느슨하게 두면 다른 질문에 엉뚱한 답을 줍니다.