5장. Decoder-only LLM과 학습 단계
최신 LLM 구조는 무엇이 달라졌나
자동차의 기본 뼈대(엔진, 바퀴 네 개, 운전대)는 수십 년째 같지만 점화 플러그나 변속기 같은 부품은 계속 더 효율적인 것으로 바뀝니다. LLM 도 같습니다. Decoder 블록을 반복하는 뼈대는 GPT 와 같지만, 정규화, 위치 인코딩, FFN 같은 세부 부품은 효율을 위해 많이 바뀌었습니다. Qwen 도 지금까지 배운 Decoder-only 구조의 핵심(Masked Self-Attention + FFN 반복)은 그대로입니다.
그림의 네 부품은 모두 실습의 print(model) 출력에서 눈으로 찾을 수 있습니다. 왼쪽부터 정규화, 위치, FFN, Attention 자리의 부품입니다. 바뀐 까닭은 대부분 "같은 성능을 더 적은 계산과 메모리로" 얻기 위해서입니다.
print(model) 에서 찾는 새 부품
| 부품 | 원조 Transformer(3~4차시) | Qwen 의 부품 | print(model) 에서 보이는 곳 | 쉽게 말하면 |
|---|---|---|---|---|
| 정규화 | LayerNorm(평균 빼고 표준편차로 나눔) | RMSNorm(제곱 평균의 제곱근으로만 나눔) | Qwen2RMSNorm((896,), eps=1e-06) | 눈금 맞추기를 한 단계 줄임 |
| 위치 정보 | 사인·코사인 값을 임베딩에 더함(2장) | RoPE(Q·K 벡터를 위치만큼 회전) | (rotary_emb): Qwen2RotaryEmbedding() | 번호표 대신 시계 바늘 각도 |
| FFN | Linear → ReLU → Linear | SwiGLU(gate·up 두 갈래, SiLU) | gate_proj, up_proj, down_proj, SiLUActivation() | 문지기가 붙은 개인 과외 |
| Attention | Q·K·V 머리 수가 같음 | GQA(Q 14개가 K·V 2개를 나눠 씀) | k_proj, v_proj 의 out_features=128 | 참고서 두 권을 일곱 명씩 돌려 보기 |
| 정규화 위치 | 블록 뒤(Add 다음 Norm) | 블록 앞(먼저 Norm, 그다음 Attention·FFN) | input_layernorm, post_attention_layernorm | 재료를 먼저 다듬고 요리 |
GQA 의 숫자를 손으로 따라가 봅니다. hidden 896 을 머리 14개로 나누면 머리 하나가 896 ÷ 14 = 64칸입니다. q_proj 의 출력이 896 이므로 Query 머리는 14개입니다. k_proj 의 출력은 128 이므로 Key 머리는 128 ÷ 64 = 2개입니다. Query 14개가 Key·Value 2개를 14 ÷ 2 = 7개씩 나눠 씁니다. 그 덕분에 k_proj 의 가중치는 896 × 128 = 114,688개로, q_proj 의 896 × 896 = 802,816개의 7분의 1 입니다.
짧은 예시
import numpy as np
# RMSNorm 을 손으로: 벡터 [3, 4] 의 눈금 맞추기
x = np.array([3.0, 4.0])
# 제곱 평균 = (9 + 16) / 2 = 12.5, 그 제곱근 = 약 3.5355
rms = np.sqrt(np.mean(x ** 2))
print(round(rms, 4)) # 결과: 3.5355
# 1e-6 은 0 으로 나누지 않게 더하는 아주 작은 수(print(model) 의 eps=1e-06)
print(np.round(x / np.sqrt(np.mean(x ** 2) + 1e-6), 4)) # 결과: [0.8485 1.1314]
# SiLU = x × sigmoid(x). sigmoid(x) = 1 / (1 + e^-x)
def silu(v):
return v / (1 + np.exp(-v))
print(np.round(silu(np.array([-2.0, 0.0, 2.0])), 4)) # 결과: [-0.2384 0. 1.7616]
# GQA 머리 수 계산: print(model) 의 숫자만으로
hidden, q_out, k_out, n_heads = 896, 896, 128, 14
head_dim = hidden // n_heads # 머리 하나의 칸 수
kv_heads = k_out // head_dim # Key·Value 머리 수
print(head_dim, kv_heads, n_heads // kv_heads) # 결과: 64 2 7이 코드가 뜻하는 것
새 부품 세 개를 손계산으로 열어 보았습니다. RMSNorm 은 [3, 4] 를 3.5355 로 나눠 [0.8485, 1.1314] 로 눈금을 맞춥니다. 평균을 빼지 않으므로 LayerNorm 보다 계산이 한 단계 적습니다. SiLU 는 2 를 1.7616 으로 거의 살리고 -2 를 -0.2384 로 크게 줄입니다. GQA 는 숫자 64, 2, 7 로 정리됩니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
np.sqrt(np.mean(x ** 2)) | 제곱 평균의 제곱근(RMS) | 숫자들의 평균 크기 |
x / ... | RMS 로 나누기 | 눈금 맞추기 |
v / (1 + np.exp(-v)) | x × sigmoid(x) | 양수는 통과, 음수는 줄이기 |
k_out // head_dim | 128 ÷ 64 | Key·Value 머리 수 2 |
실제 RMSNorm 은 나눈 뒤 학습된 가중치(896개)를 곱합니다. 이 가중치가 print(model) 의 (896,) 이고, 프로젝트에서 Norm 묶음의 파라미터로 셉니다.
주의할 점
- 노트북은 "Qwen 도 세부 구현은 조금씩 다르다"고만 말합니다. 부품 이름은 모델마다,
transformers버전마다 조금 다르게 찍힐 수 있습니다(예:SiLUActivation()대신SiLU()). 구조를 읽을 때는 이름보다in_features,out_features숫자를 봅니다. - 부품이 바뀌어도 4장의 Causal Mask 는 그대로 쓰입니다.
print(model)은 층 모양만 보여 주므로 Mask 는 출력에 나오지 않습니다.