LLM 엔지니어링16장 · vLLM 서빙 벤치마킹과 RAG 최적화4 / 22← 이전목차다음 →MetaContents

16장. vLLM 서빙 벤치마킹과 RAG 최적화

프리필과 디코드

식당에서 주문서를 받으면 요리사는 먼저 주문서 전체를 한 번에 훑어 재료를 꺼냅니다. 그다음에는 접시에 하나씩 담아 나갑니다. LLM 의 생성도 두 단계입니다. 입력 전체를 한꺼번에 읽는 프리필과, 토큰을 하나씩 이어 쓰는 디코드입니다.

입력 프롬프트를 한꺼번에 읽는 프리필과 토큰을 하나씩 이어 쓰는 디코드를 견준 비교 그림

왼쪽 프리필은 입력 토큰을 모두 한 번에 계산하므로 GPU 의 계산 능력을 꽉 씁니다. 오른쪽 디코드는 토큰 하나를 낼 때마다 가중치 전체를 메모리에서 다시 읽어야 해서, 계산보다 메모리를 읽는 속도에 묶입니다(15장). 주문서 훑기가 프리필, 한 접시씩 담기가 디코드입니다.

두 단계 견주기

프리필디코드
하는 일입력 토큰을 한꺼번에 계산해 KV 캐시를 채운다토큰을 하나씩 만든다
시간을 정하는 것입력 길이출력 길이
묶이는 곳GPU 계산메모리 읽기
사용자가 느끼는 것첫 글자가 늦게 나온다(TTFT)글이 천천히 나온다(TPOT)

시간 어림하기

Python
def gen_seconds(prompt_tokens, output_tokens, prefill_speed, decode_speed):
    """프리필은 입력 토큰을 빨리, 디코드는 출력 토큰을 하나씩 처리한다고 보고 시간을 어림한다"""
    prefill = prompt_tokens / prefill_speed
    decode = output_tokens / decode_speed
    return round(prefill, 2), round(decode, 2)

# 속도는 예시 숫자다(프리필이 디코드보다 열 배쯤 빠르다고 둔다)
for p, o in [(100, 50), (1500, 50), (100, 500)]:
    print(f"입력 {p:5d} · 출력 {o:4d} → (프리필 초, 디코드 초) =", gen_seconds(p, o, prefill_speed=1000, decode_speed=50))

위 예시의 속도는 설명을 위한 숫자입니다. 이 PC 의 실제 값은 실습 1에서 잽니다.

코드에 나오는 함수

코드쉬운 뜻
gen_seconds(prompt_tokens, output_tokens, prefill_speed, decode_speed)gen 은 generation(생성)의 줄임말이다. 입력·출력 토큰 수와 두 단계의 빠르기로 걸릴 시간을 어림하는 함수다
prompt_tokens / prefill_speed입력 토큰 수를 1초에 읽는 토큰 수로 나눈다. 프리필에 걸리는 초다
output_tokens / decode_speed출력 토큰 수를 1초에 쓰는 토큰 수로 나눈다. 디코드에 걸리는 초다
round(prefill, 2)소수 둘째 자리까지 반올림한다
prefill_speed=10001초에 입력 1000토큰을 읽는다고 둔다(예시 숫자)
decode_speed=501초에 출력 50토큰을 쓴다고 둔다(예시 숫자)

이 코드가 뜻하는 것

식당에서 주문서는 눈으로 한 번에 훑지만, 접시는 하나씩 담아야 합니다. 주문서가 길어지면 훑는 시간이 조금 늘고, 접시가 많아지면 담는 시간이 크게 늡니다. 여기서 주문서의 줄 수가 입력 토큰, 담을 접시 수가 출력 토큰입니다.

줄하는 일쉽게 말하면
입력 100 · 출력 50프리필 0.1초, 디코드 1.0초짧은 주문서, 접시 50개
입력 1500 · 출력 50프리필 1.5초, 디코드 1.0초주문서가 15배 길어져 훑는 시간이 늘었다
입력 100 · 출력 500프리필 0.1초, 디코드 10.0초접시가 10배 많아져 담는 시간이 10배다

이 예시의 빠르기에서는 같은 토큰 수라도 출력 쪽이 훨씬 비쌉니다. 입력 1500토큰은 1.5초인데, 출력 500토큰은 10초가 걸립니다. 그래서 답 길이를 줄이는 것이 전체 시간을 줄이는 데 가장 잘 듣습니다.

작게 줄여서 눈으로 보기

KV 캐시에 칸이 어떻게 쌓이는지 걸음으로 봅니다. 토큰은 설명을 위해 지어낸 것입니다.

Python
prompt = ["연차", "는", "며칠", "인가요"]      # 입력 토큰 넷(설명을 위해 지어낸 토큰)
answer = ["15", "일", "입니다"]               # 모델이 쓸 답 토큰 셋(지어낸 것)

kv = list(prompt)                              # 프리필: 입력 넷을 한 걸음에 모두 KV 캐시에 넣는다
print(f"프리필 1걸음 → KV 캐시 {len(kv)}칸")
for step, tok in enumerate(answer, start=1):   # 디코드: 한 걸음에 토큰 하나씩
    kv.append(tok)
    print(f"디코드 {step}걸음 → '{tok}' 추가, KV 캐시 {len(kv)}칸")
출력
프리필 1걸음 → KV 캐시 4칸
디코드 1걸음 → '15' 추가, KV 캐시 5칸
디코드 2걸음 → '일' 추가, KV 캐시 6칸
디코드 3걸음 → '입니다' 추가, KV 캐시 7칸
결과읽는 법
프리필 1걸음 → KV 캐시 4칸입력 토큰 넷을 한 걸음에 모두 넣었다
디코드 1걸음부터 3걸음답 토큰을 걸음마다 하나씩 더한다. 답이 길수록 걸음이 는다
KV 캐시 7칸입력 4 + 출력 3 이다. 다음 토큰을 만들 때 앞의 칸을 다시 계산하지 않고 꺼내 쓴다

주의할 점

  • 입력이 길면 첫 토큰이 늦고, 출력이 길면 전체가 늦습니다. RAG 는 검색한 조각을 붙여 입력을 길게 만들므로 프리필 시간이 커집니다.
  • 답을 짧게 하라는 지시는 디코드 시간을 줄이는 가장 쉬운 방법입니다(실습 6).
← 이전 절16장 · 서빙 엔진이 하는 일다음 절 →16장 · KV 캐시와 PagedAttention