LLM Inference Overview
ChatGPT에 질문을 던지면 답변이 줄줄 흘러나온다. 문장을 한 번에 떠올리는 것처럼 보이지만, 실제로는 토큰 하나를 만들고 입력 끝에 붙이는 걸 반복하는 것뿐이다.
Autoregressive Generation
LLM은 다음에 올 토큰의 확률 분포를 계산하는 함수다.
토큰 시퀀스를 받아서 어휘(vocabulary) 전체에 대한 확률 분포를 내놓는다.
한 문장을 만들 때 이 함수가 한 번 호출되는 게 아니다. 토큰 하나를 뽑을 때마다 한 번씩, 답변 길이만큼 반복 호출된다.
방금 만든 토큰을 다시 입력에 넣어 다음 토큰을 만드는 이 방식을 autoregressive(자기회귀)라 한다.
“한국의 수도는”이라는 프롬프트에 “서울입니다”라고 답한다면:
flowchart LR
P["한국의 수도는"] --> S1["한국의 수도는 + 서울"]
S1 --> S2["...서울 + 입니다"]
S2 --> S3["...입니다 + ."]
S3 --> END["EOS"]
매 단계에서 모델은 그때까지의 전체 시퀀스를 입력으로 받아 다음 토큰을 계산한다. 100토큰짜리 답변이면 100번의 forward pass다.
GPU가 바쁜 이유, VRAM이 많이 필요한 이유, vLLM 같은 서빙 프레임워크가 존재하는 이유는 전부 이 반복을 효율적으로 돌리는 문제로 귀결된다.
Tokenizer
모델은 글자나 단어가 아닌 토큰 단위로 동작한다. 어휘 집합은 보통 32k~256k개다.
“한국의 수도는”은 tokenizer가 [14302, 9821, 3145] 같은 정수 ID 수열로 바꾼다.
이 정수들이 모델의 실제 입력이다.
한국어는 영어처럼 공백으로 단어가 잘 나뉘지 않아서, 같은 의미라도 토큰 수가 두세 배 많아지는 경우가 흔하다.
API 과금이 한국어에서 더 비싼 이유
Forward Pass
Forward pass란 입력을 모델에 한 번 통과시켜 출력을 얻는 계산을 말한다.
데이터가 입력에서 출력 방향(순방향)으로 흐른다는 뜻이다.
학습할 때는 출력의 오차를 거꾸로 흘려보내는 backward pass(역전파)가 따라붙지만, inference에는 forward pass만 있다.
토큰이 들어가서 확률 분포가 나오기까지, 안에서 벌어지는 일은 이렇다:
flowchart TD
A["토큰 ID 수열"] --> B["임베딩 lookup"]
B --> C["Transformer block × N"]
C --> D["LM head"]
D --> E["logits (vocab 크기)"]
E --> F["softmax → 확률 분포"]
임베딩 lookup
토큰 ID는 14302 같은 정수일 뿐이라 그 자체로는 의미가 없다.
14302가 9821보다 크다는 게 아무것도 뜻하지 않는다.
그래서 각 토큰을 숫자 여러 개의 목록, 즉 벡터로 바꾼다.
색을 떠올리면 쉽다. 색에 이름표(빨강=1, 주황=2)만 붙이면 1번과 2번이 비슷한지 알 수 없다.
대신 (R, G, B) 세 숫자로 표현하면 빨강 (255,0,0)과 주황 (255,128,0)이 가깝다는 걸 계산할 수 있다.
임베딩은 같은 일을 숫자 4096개로 한다. 토큰 하나가 4096차원 공간의 한 점이 되고, 의미가 비슷한 토큰(왕/여왕, 서울/부산)끼리 가까이 모인다.
이 4096이 hidden size인데, 어휘가 128k개면 임베딩 테이블은 128k × 4096 매트릭스이고, 토큰 ID는 이 테이블의 몇 번째 행을 꺼낼지 가리키는 행 번호다.
Transformer block × N
층(block) 하나는 벡터를 받아 조금 가공한 벡터를 내놓는 함수다. 그 출력을 다음 층의 입력으로 넣는다. “층을 통과한다”는 건 이 과정을 N번 반복한다는 뜻이다.
공장 컨베이어 벨트를 떠올리면 된다. 각 공정이 조금씩 다듬어 다음 공정으로 넘긴다.
앞쪽 층은 문법 같은 표층 패턴을, 뒤쪽 층은 의미나 맥락 같은 추상적 패턴을 잡는다고 알려져 있다.
층 하나는 두 부분으로 나뉜다.
- Attention. 각 토큰이 문장의 다른 토큰들 중 어디를 봐야 할지 정하고 그 정보를 끌어온다. “수도”가 “한국”을 참조해 의미를 좁히는 식이다.
- FFN(feed-forward network). 각 토큰을 독립적으로 한 번 더 가공하는 작은 신경망이다.
Attention과 FFN의 자세한 동작은 넘어간다.
보통 Llama-3 8B, Qwen 32B 같은 식으로 모델 이름 뒤에 숫자를 붙이는데, 이 숫자가 파라미터(parameter) 개수다.
파라미터란 학습으로 정해진 모델 내부의 숫자(weight)를 말한다. 8B = 80억 개, 70B = 700억 개이고 B는 billion(10억)이다.
파라미터가 많을수록 표현력이 좋지만 메모리와 연산도 그만큼 늘어난다.
층 수도 함께 늘어서 Llama-3 8B는 32층, 70B는 80층이다. VRAM을 얼마나 먹는지는 결국 이 파라미터 개수에서 출발한다.
LM head
지금까지의 결과를 정리하면, 입력 토큰 하나하나가 층을 다 통과한 뒤 각자 벡터 하나씩(4096개 숫자)을 들고 나온다. 토큰 5개를 넣었으면 벡터 5개가 나온다.
그런데 우리가 알고 싶은 건 “이 문장 다음에 올 토큰”이다. 그래서 이 중 맨 마지막 토큰 위치의 벡터 하나만 본다.
마지막 위치는 attention을 통해 앞의 모든 토큰을 이미 참조했기 때문에, “여기까지 다 읽었을 때 그 다음은?”이라는 질문의 답을 담고 있다.
문제는 이 벡터가 4096개 숫자일 뿐이라, 그대로는 “어떤 토큰이 다음이냐”를 알 수 없다는 것이다.
LM head는 이 4096차원 벡터에 매트릭스를 곱해서 어휘 개수만큼의 점수 목록으로 펼친다.
어휘가 128k개면 숫자 128k개가 나오고, 각 숫자는 “그 토큰이 다음에 올 후보로서 받은 점수”다.
이 정규화 안 된 점수를 logits라 한다. 점수일 뿐 아직 확률이 아니라서, 다음 단계인 softmax가 필요하다.
Softmax
logits는 그냥 점수라서 다 더해도 1이 아니다. softmax는 이 점수들을 합이 1인 확률로 바꾸는 함수다. 점수가 높을수록 큰 확률을 받는다.
모든 토큰 위치에 대해 계산하지만, 다음 토큰 예측에는 마지막 위치의 출력만 쓴다.
여기서 KV cache라는 아이디어가 나온다. 이미 계산된 위치의 중간값을 매번 다시 만들 이유가 없다.
Sampling
확률 분포에서 토큰 하나를 뽑는 단계다.
가장 단순한 방식은 확률 최대인 토큰을 고르는 greedy decoding이지만, 같은 입력에 같은 답만 나온다. 보통 다음 방법을 조합한다.
- temperature. 분포를 평평하게(다양) 또는 뾰족하게(보수적) 스케일링
- top-k. 상위 k개 토큰만 후보로 남기고 샘플링
- top-p (nucleus). 누적 확률 p까지의 토큰만 후보로 남김
토큰 하나가 정해지면 입력 끝에 붙이고 처음부터 다시 시작한다.
Stop Condition
정지 조건은 두 가지다.
- 모델이 EOS 토큰(end-of-sequence)을 뽑은 경우
- max_tokens 한도에 도달한 경우
채팅 인터페이스의 streaming도 같은 과정이다. 토큰이 생성될 때마다 즉시 클라이언트로 전송할 뿐이다.
정리
이 한 사이클이 forward pass 한 번이며, 답변 토큰 수만큼 반복된다.
VRAM이 어디로 가는지, KV cache가 왜 필요한지, prefill과 decode가 왜 다른 동작인지. 모두 이 사이클의 어느 부분을 들여다보느냐의 차이다.