5장. Decoder-only LLM과 학습 단계
Qwen 설계도 읽기
가구를 조립할 때 설명서 첫 장에는 부품 목록이 있습니다. "긴 나사 24개, 선반 2장"처럼 이름과 개수, 크기가 적혀 있습니다. print(model) 이 바로 그 부품 목록입니다. 괄호 안의 이름이 부품, (0-23): 24 x 가 개수, in_features, out_features 가 크기입니다. Qwen 은 Alibaba Cloud 가 공개한 Decoder-only LLM 패밀리로, 0.5B 부터 72B 까지 여러 크기가 있고 Apache 2.0 라이선스라 상업적으로도 쓸 수 있습니다. 이 과정은 가장 작은 Qwen2.5-0.5B-Instruct(약 4.9억 개)를 씁니다.
그림은 실습에서 찍을 print(model) 을 위에서 아래로 읽는 차례입니다. 토큰 번호가 임베딩 표에서 896칸 성격표가 되고, 같은 Decoder 블록 24개를 차례로 지난 뒤, lm_head 에서 어휘 151,936개의 점수표가 됩니다.
print(model) 의 줄마다 뜻
| 줄 | 모양 | 뜻 | 쉽게 말하면 |
|---|---|---|---|
Qwen2ForCausalLM | 겉 상자 | 다음 토큰 맞히기(Causal LM)용 Qwen2 | 제품 이름 |
(embed_tokens): Embedding(151936, 896) | 151,936 × 896 | 토큰 번호마다 숫자 896개짜리 행 | 행 15만 개, 열 896개인 DataFrame |
(0-23): 24 x Qwen2DecoderLayer | 24층 | 같은 모양의 Decoder 블록 24개 | 같은 선반 24장 |
q_proj | 896 → 896, bias 있음 | Query 만들기(머리 14개 × 64) | 찾는 말 |
k_proj, v_proj | 896 → 128, bias 있음 | Key·Value 만들기(머리 2개 × 64, GQA) | 제목표와 내용 |
o_proj | 896 → 896, bias 없음 | 머리들의 결과를 다시 896칸으로 합치기 | 전문가 의견 합치기 |
gate_proj, up_proj | 896 → 4864 | FFN 에서 칸을 5.4배로 넓히는 두 갈래 | 넓은 작업대 두 개 |
down_proj | 4864 → 896 | 넓힌 칸을 다시 896 으로 줄이기 | 결과를 원래 크기로 |
act_fn: SiLUActivation() | 숫자 없음 | gate 쪽에 거는 SiLU | 문지기 |
input_layernorm, post_attention_layernorm | (896,) | Attention 앞, FFN 앞의 RMSNorm | 요리 전 재료 다듬기 |
(norm) | (896,) | 24층 뒤의 마지막 RMSNorm | 마지막 눈금 맞추기 |
(rotary_emb) | 숫자 없음 | RoPE 위치 정보 | 시계 바늘 각도 |
(lm_head): Linear(896, 151936) | 896 → 151,936 | 다음 토큰 점수표 만들기 | 후보 15만 개에 점수 매기기 |
Embedding(151936, 896) 은 파라미터가 151,936 × 896 = 136,134,656개입니다. 토크나이저가 아는 토큰은 151,665개인데 표의 행은 151,936개입니다. 남는 행은 계산을 빠르게 하려고 크기를 맞춰 둔 빈자리입니다. lm_head 는 임베딩 표와 같은 숫자를 함께 씁니다(tie_word_embeddings). 그래서 파라미터 수를 셀 때 한 번만 셉니다. 프로젝트에서 확인합니다.
짧은 예시
# AutoConfig = 모델의 설정표(config.json)만 불러오는 도구. 가중치는 내려받지도 올리지도 않는다
from transformers import AutoConfig
cfg = AutoConfig.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
# 설정표에서 print(model) 의 숫자들을 꺼낸다
h, ffn, vocab, layers = cfg.hidden_size, cfg.intermediate_size, cfg.vocab_size, cfg.num_hidden_layers
kv = cfg.num_key_value_heads * (h // cfg.num_attention_heads) # 2 × 64 = 128
print(h, ffn, vocab, layers, kv) # 결과: 896 4864 151936 24 128
# 층 하나의 파라미터를 손으로: 가중치(입력 × 출력) + bias(출력)
attn = (h * h + h) + (h * kv + kv) * 2 + h * h # q(bias 있음) + k, v(bias 있음) + o(bias 없음)
mlp = h * ffn * 3 # gate, up, down 모두 bias 없음
norm = h * 2 # RMSNorm 두 개
print(f"Attention {attn:,} / FFN {mlp:,} / Norm {norm:,}")
# 결과: Attention 1,836,160 / FFN 13,074,432 / Norm 1,792
# 전체 = 임베딩 + 24층 + 마지막 norm (lm_head 는 임베딩 표를 함께 써서 더하지 않는다)
total = vocab * h + (attn + mlp + norm) * layers + h
print(f"전체 {total:,}개, BF16 메모리 {total * 2 / 1e9:.2f} GB")
# 결과: 전체 494,032,768개, BF16 메모리 0.99 GB이 코드가 뜻하는 것
가구 설명서의 부품 목록만 보고 나사 개수를 센 것과 같습니다. 모델을 불러오지 않고 설정표의 숫자 다섯 개(896, 4864, 151936, 24, 128)만으로 전체 파라미터 494,032,768개를 맞혔습니다. 손으로 따라가 보면 FFN 하나가 896 × 4864 = 4,358,144개이고 세 개면 13,074,432개입니다. 층 하나 안에서 FFN 이 Attention(1,836,160개)의 약 7배입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
AutoConfig.from_pretrained(...) | config.json 만 읽기 | 설명서 첫 장만 펴기 |
(h * h + h) | q_proj 의 가중치 + bias | 896 × 896 + 896 |
h * ffn * 3 | gate, up, down | 큰 작업대 세 개 |
vocab * h + (...) * layers + h | 임베딩 + 24층 + 마지막 norm | 부품 전체 합 |
total * 2 / 1e9 | BF16 은 숫자 하나에 2바이트 | 메모리 = 파라미터 수 × 바이트 수 |
메모리는 494,032,768 × 2바이트 = 988,065,536바이트, 약 0.99GB 입니다. 같은 모델을 FP32(4바이트)로 올리면 약 1.98GB, 4비트(0.5바이트)로 줄이면 약 0.25GB 입니다. 9장의 QLoRA 가 이 계산을 씁니다.