트랜스포머·PEFT 2일 과정5장 · Decoder-only LLM과 학습 단계6 / 15← 이전목차다음 →MetaContents

5장. Decoder-only LLM과 학습 단계

최신 LLM 구조는 무엇이 달라졌나

자동차의 기본 뼈대(엔진, 바퀴 네 개, 운전대)는 수십 년째 같지만 점화 플러그나 변속기 같은 부품은 계속 더 효율적인 것으로 바뀝니다. LLM 도 같습니다. Decoder 블록을 반복하는 뼈대는 GPT 와 같지만, 정규화, 위치 인코딩, FFN 같은 세부 부품은 효율을 위해 많이 바뀌었습니다. Qwen 도 지금까지 배운 Decoder-only 구조의 핵심(Masked Self-Attention + FFN 반복)은 그대로입니다.

RMSNorm, RoPE, SwiGLU, GQA 네 부품을 나란히 놓은 비교 그림

그림의 네 부품은 모두 실습의 print(model) 출력에서 눈으로 찾을 수 있습니다. 왼쪽부터 정규화, 위치, FFN, Attention 자리의 부품입니다. 바뀐 까닭은 대부분 "같은 성능을 더 적은 계산과 메모리로" 얻기 위해서입니다.

부품원조 Transformer(3~4차시)Qwen 의 부품print(model) 에서 보이는 곳쉽게 말하면
정규화LayerNorm(평균 빼고 표준편차로 나눔)RMSNorm(제곱 평균의 제곱근으로만 나눔)Qwen2RMSNorm((896,), eps=1e-06)눈금 맞추기를 한 단계 줄임
위치 정보사인·코사인 값을 임베딩에 더함(2장)RoPE(Q·K 벡터를 위치만큼 회전)(rotary_emb): Qwen2RotaryEmbedding()번호표 대신 시계 바늘 각도
FFNLinear → ReLU → LinearSwiGLU(gate·up 두 갈래, SiLU)gate_proj, up_proj, down_proj, SiLUActivation()문지기가 붙은 개인 과외
AttentionQ·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 입니다.

짧은 예시

Python
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_dim128 ÷ 64Key·Value 머리 수 2

실제 RMSNorm 은 나눈 뒤 학습된 가중치(896개)를 곱합니다. 이 가중치가 print(model) 의 (896,) 이고, 프로젝트에서 Norm 묶음의 파라미터로 셉니다.

주의할 점

  • 노트북은 "Qwen 도 세부 구현은 조금씩 다르다"고만 말합니다. 부품 이름은 모델마다, transformers 버전마다 조금 다르게 찍힐 수 있습니다(예: SiLUActivation() 대신 SiLU()). 구조를 읽을 때는 이름보다 in_features, out_features 숫자를 봅니다.
  • 부품이 바뀌어도 4장의 Causal Mask 는 그대로 쓰입니다. print(model) 은 층 모양만 보여 주므로 Mask 는 출력에 나오지 않습니다.
← 이전 절5장 · 규모를 키우자 생긴 일: Scaling Law다음 절 →5장 · Qwen 설계도 읽기