DevOps··3분 읽기·6

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개 숫자가 실제로 뭘 의미하는지, 한국어가 왜 토큰 효율이 낮은지.

이 글이 어떠셨나요?

이 글이 도움이 되셨나요?
공유:

뉴스레터 구독

새 글이 올라오면 이메일로 알려드려요.

댓글

댓글을 불러오는 중...