1. LLM은 텍스트를 어떻게 처리하는가
LLM이 텍스트 한 줄을 받아서 다음 토큰을 예측하기까지의 전체 과정을 4단계로 따라간다. 토크나이저, 임베딩, 트랜스포머 레이어, 출력까지 "OOM killer가 nginx를" 한 문장이 모델 안에서 겪는 여행.
ollama로 같은 로그 분석 프롬프트를 Qwen2.5 3B와 7B에 던져봤다. 결과가 확연히 달랐다.
3B는 "OOM killer가 nginx 프로세스를 종료시켰습니다" 정도의 사실 나열에 그쳤다. 7B는 거기서 한 발 더 나가서 "메모리 누수가 의심되므로 nginx의 worker_connections 설정과 메모리 사용 추이를 확인하라"는 후속 조치까지 제안했다.
같은 구조다. 같은 프롬프트다. 숫자(파라미터)만 더 많을 뿐인데 왜 결과가 이렇게 다를까?
이 의문이 시작점이었고, 파고들다 보니 시리즈가 됐다.
LLM이란 뭔가
Large Language Model. 대규모 언어 모델. 방대한 양의 데이터로 사전 학습된 초대형 딥러닝 모델이다. 이름이 거창하지만 하는 일은 단순하다. 텍스트를 받아서 "다음에 올 단어"를 예측하는 것. 이게 전부다.
ChatGPT, Claude, Gemini, ollama로 돌리는 Qwen 전부 이 원리다. "이해"하는 게 아니다. 수조 개의 텍스트에서 "이 다음에 뭐가 올 확률이 높은지"를 학습한 결과일 뿐이다.
실제로 어떻게 동작하는지 보면 이렇다.
LLM의 본질:
입력: "OOM killer가 nginx를"
출력: 다음 토큰의 확률 분포
"종료시켰다" 23% ◀── 가장 높음
"죽였다" 15%
"메모리" 11%
"해당" 8%
...
→ 하나를 고른다.
→ 고른 토큰을 입력 뒤에 붙인다.
→ 다시 예측한다.
→ 반복하면 문장이 생성된다.
"OOM killer가 nginx를" → "종료시켰다"
"OOM killer가 nginx를 종료시켰다" → "."
"OOM killer가 nginx를 종료시켰다." → "이"
"OOM killer가 nginx를 종료시켰다. 이" → "로그는"
...대화처럼 보이지만, 실제로는 "다음 단어 맞추기"를 수백~수천 번 반복하는 거다.
그러면 이 한 문장이 모델 안에서 어떤 여행을 하는지 처음부터 끝까지 따라가본다.
1단계: 토크나이저 — 텍스트를 숫자로
모델은 문자열을 이해 못한다. 숫자만 처리할 수 있다.
"OOM killer가 nginx를"
│
▼ 토크나이저 (모델 밖, CPU에서 실행)
│
["OOM", "killer", "가", "nginx", "를"]
│
▼ 어휘 사전에서 ID 조회
│
[15832, 25871, 1102, 28712, 1265]토크나이저는 모델과 별개인 프로그램이다. 텍스트를 정수 배열로 바꿔주는 변환기. "nginx"처럼 자주 나오는 단어는 통째로 하나의 토큰이 되고, 모르는 단어는 더 잘게 쪼개진다.
이 단계는 GPU가 아니라 CPU에서 실행된다. 마이크로초 만에 끝나니까 병목이 아니다.
2단계: 임베딩 — 숫자를 벡터로
정수 배열을 모델에 넣으면, 가장 먼저 하는 일이 임베딩 테이블 조회다.
[15832, 25871, 1102, 28712, 1265]
│
▼ 임베딩 테이블 (15만행 × 2048열의 숫자표)
│
│ 15832번 행 → [0.823, 0.154, -0.431, ..., 0.551]
│ 25871번 행 → [0.341, -0.562, 0.187, ..., 0.892]
│ ...
│
▼
벡터 5개 (각 2048개의 실수)토큰 ID로 거대한 숫자표를 조회하면, 2048개의 실수로 된 벡터가 나온다. 이 벡터가 해당 토큰의 "의미"를 담은 좌표다. 비슷한 의미의 토큰은 가까운 좌표에 위치한다.
이 시점에서 각 벡터는 개별 토큰의 의미만 담고 있다. "OOM" 뒤에 "killer"가 온다는 건 아직 모른다. 이 테이블 자체가 학습된 파라미터인데, 15만 × 2048 = 약 3억 개의 숫자다.
3단계: 트랜스포머 레이어 — 여기가 핵심이다
여기서부터가 LLM의 진짜 두뇌다.
벡터 5개 (각 토큰이 서로의 존재를 모르는 상태)
│
▼
┌─ 레이어 1 ───────────────────────────────────┐
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 어텐션 │ ───→ │ FFN │ │
│ │ │ │ │ │
│ │ 각 토큰이 │ │ "이게 무슨 │ │
│ │ 다른 토큰을 │ │ 의미인지" │ │
│ │ 참조한다 │ │ 변환한다 │ │
│ └──────────┘ └──────────┘ │
└────────────────────────────────────────────────┘
│
▼ (같은 구조를 36번 반복, 매번 다른 파라미터)
│
┌─ 레이어 36 ──────────────────────────────────┐
│ 어텐션 → FFN │
└────────────────────────────────────────────────┘
│
▼
벡터 5개 (차원은 여전히 2048이지만, 담고 있는 의미가 완전히 다름)어텐션은 각 토큰이 다른 토큰을 "참조"해서 자기 벡터에 정보를 섞는 연산이다. "killer"의 벡터가 "OOM"의 정보를 흡수하면 "OOM과 관련된 killer"로 업데이트된다. 핵심은 Q(질문)·K(색인)·V(내용) 세 벡터의 내적과 가중합인데, 이건 3편에서 상세히 다룬다.
FFN은 관계가 파악된 벡터를 받아서 "이게 무슨 의미인지" 변환하는 연산이다. "nginx + OOM killer의 대상"이라는 관계 정보를 "웹 서버가 메모리 부족으로 죽은 상황"이라는 의미로 변환한다. "nginx = 웹 서버", "OOM = 메모리 부족" 같은 지식이 여기 저장되어 있다. 4편에서 상세히.
어텐션 + FFN = 1개 레이어다. 이게 36번 반복되면서 이해가 점점 깊어진다.
- 앞쪽 레이어: "가"→주격, "를"→목적격 (문법)
- 중간 레이어: "OOM"→메모리, "nginx"→서버 (의미)
- 뒤쪽 레이어: "메모리 부족 → 서버 종료 → 서비스 장애" (추론)
레이어마다 다른 파라미터를 쓴다. 같은 구조인데 역할이 다른 이유다.
4단계: 출력 — 벡터에서 다음 토큰으로
36개 레이어를 통과한 마지막 벡터가 토큰이 되는 과정이다.
마지막 레이어의 마지막 토큰("를") 위치 벡터
│
▼ × 출력 행렬 (2048 × 151,936)
│
151,936개의 점수 (어휘 전체에 하나씩)
│
▼ softmax (점수를 확률로 변환)
│
확률 분포:
"종료시켰다" 23% ◀── 선택
"죽였다" 15%
"메모리" 11%
"해당" 8%
...마지막 토큰 위치의 벡터(2048차원)에 출력 행렬을 곱하면, 15만 개 토큰 각각에 대한 점수가 나온다. softmax로 확률로 바꾸고, 그중에서 하나를 고른다. 이걸 다시 입력에 붙여서 반복한다.
매 토큰마다 이 전체 과정(임베딩 → 36개 레이어 → 출력)을 다시 수행한다.
전체 여행 요약
"OOM killer가 nginx를"
│
│ ① 토크나이저: 텍스트 → 정수 5개
▼
│ ② 임베딩: 정수 → 벡터 5개 (각 2048차원, 개별 의미만)
▼
│ ③ 레이어 ×36: 벡터끼리 정보 교환(어텐션) + 의미 변환(FFN)
▼
│ ④ 출력: 마지막 벡터 → 15만 토큰의 확률 → "종료시켰다" 선택
▼
"종료시켰다" → 입력 뒤에 붙여서 ①부터 다시 반복그래서 3B와 7B의 차이는 어디서 오는가
위 과정에서 "임베딩 테이블", "어텐션의 W_Q/W_K/W_V", "FFN의 W1/W2", "출력 행렬" 전부 학습으로 조정된 숫자다. 이걸 파라미터라고 한다.
3B 모델은 이 숫자가 30억 개, 7B는 70억 개다. 숫자가 더 많으면 더 풍부한 의미를 담을 수 있고, 더 미세한 패턴을 감지할 수 있고, 더 깊은 추론이 가능하다.
3B가 헛소리한 건 파라미터가 부족해서다. "OOM killer가 뭔지"는 알지만 "이게 왜 위험하고 어떤 조치가 필요한지"까지 추론할 여력이 없었던 거다. 이 파라미터들이 VRAM에 올라가야 추론이 가능한데, 3B FP16 기준 약 6GB, 7B FP16이면 약 14GB다.
다음 편에서는 1단계(토크나이저)와 2단계(임베딩)를 깊이 파고든다. BPE가 어떻게 어휘 사전을 만드는지, 벡터의 2048개 숫자가 실제로 뭘 의미하는지, 한국어가 왜 토큰 효율이 낮은지.
이 글이 어떠셨나요?
뉴스레터 구독
새 글이 올라오면 이메일로 알려드려요.