Chapter 06

Transformer 블록

5장의 어텐션은 토큰끼리 정보를 주고받는 장치일 뿐이다. 이것만으로는 언어 모델이 되지 않는다. 순서를 알려 줄 위치 정보, 수십~백여 층을 쌓아도 신호와 기울기가 살아남게 하는 잔차 연결과 정규화, 토큰 하나하나를 깊게 가공하는 FFN이 함께 있어야 한다. 이 장에서는 오늘날 GPT·Llama 계열이 쓰는 decoder-only Transformer 블록을 부품 단위로 뜯어 보고, 마지막에는 Llama 3 8B의 파라미터 80억 개를 직접 한 자리까지 세어 본다.

전체 그림: 인코더-디코더에서 decoder-only로

2017년 구글의 논문 "Attention Is All You Need"가 제안한 Transformer는 원래 기계 번역 모델이었다. 원문(예: 영어)을 읽는 인코더(encoder)와 번역문(예: 독일어)을 한 토큰씩 써 나가는 디코더(decoder) 두 덩어리로 되어 있었다. 인코더의 어텐션은 양방향이라 모든 원문 토큰이 서로를 본다. 디코더는 자기가 지금까지 쓴 토큰만 보는 인과적(masked) 셀프 어텐션과, 인코더 출력을 들여다보는 크로스 어텐션(cross-attention)을 함께 가진다. 원 논문의 기본 모델은 dmodel = 512, 인코더·디코더 각 6층, 헤드 8개, FFN 2048차원이었다.

그 뒤 길이 갈렸다. BERT(2018)는 인코더만 떼어 내 문장 이해에, T5(2019)는 인코더-디코더를 그대로 텍스트-투-텍스트 과제에 썼다. 그리고 GPT(2018) 계열은 디코더만 남겼다. 크로스 어텐션이 빠지면 "원문"과 "번역문"의 구분이 없어지고, 프롬프트와 생성 중인 답이 하나의 토큰 열로 이어진다. 모든 문제를 "다음 토큰 예측"(7장) 하나로 풀 수 있다는 단순함, 그리고 학습 효율 덕분에 GPT-3·4, Llama, Claude, Gemini 같은 오늘날의 대형 언어 모델은 거의 모두 decoder-only 구조다.

원 논문 (2017): 인코더-디코더 오늘날 LLM: decoder-only N× 인코더 FFN Self-Attention 양방향 원문 토큰 (영어) N× 디코더 FFN Cross-Attention 인코더 출력을 봄 Masked Self-Attn 인과적 다음 번역 토큰 지금까지의 번역문 (독일어) K, V N× 디코더 블록 FFN Masked Self-Attn 인과적 다음 토큰 확률 프롬프트 + 지금까지 생성한 토큰
그림 6-1. 원래의 Transformer(왼쪽)는 원문을 읽는 인코더와 번역문을 쓰는 디코더로 나뉘고, 디코더는 크로스 어텐션으로 인코더의 K, V를 본다. decoder-only(오른쪽)는 크로스 어텐션을 없애고 하나의 토큰 열에 인과적 셀프 어텐션과 FFN만 반복한다. 잔차 연결과 정규화는 생략하고 그렸다.

decoder-only 모델 하나를 위에서 아래까지 따라가면 다음과 같다. 토큰 ID가 임베딩 표(4장)를 거쳐 d차원 벡터가 되고, 그 벡터가 N개의 똑같은 모양의 블록을 차례로 통과한다. 블록마다 두 개의 하위 층(어텐션, FFN)이 있고, 각 하위 층은 입력을 정규화해서 읽고, 계산한 결과를 원래 벡터에 더한다. 마지막 블록을 지난 벡터를 한 번 더 정규화한 뒤, LM head(d × V 행렬)로 어휘 크기 V만큼의 로짓을 만들고 softmax로 다음 토큰 확률을 얻는다(8장).

토큰 ID n개 임베딩 V×d × N 층 (Transformer 블록, 모두 같은 구조·다른 가중치) RMSNorm Attention 인과적, GQA + RMSNorm FFN SwiGLU + 잔차 스트림 (d차원 벡터가 층을 관통) ≈ 4d² (MHA 기준) 3·d·d_ff Norm LM head d×V softmax p(다음 토큰) 각 하위 층: 정규화해서 읽고 → 계산 → 원래 벡터에 더한다. 위치 정보(RoPE)는 Attention 안에서 Q, K에 들어간다.
그림 6-2. 오늘날의 decoder-only Transformer(Llama 계열). 굵은 선이 잔차 스트림이고, 블록은 거기서 값을 읽어 계산한 결과를 더해 놓는다. GPT-2는 RMSNorm 대신 LayerNorm, SwiGLU 대신 GELU FFN, RoPE 대신 학습된 위치 임베딩을 쓰지만 뼈대는 같다.
구성 요소원 논문 (2017)GPT-2 (2019)Llama 3 (2024)
구조인코더-디코더decoder-onlydecoder-only
위치 정보사인파 PE (입력에 더함)학습된 위치 임베딩 (1024개)RoPE (base 500,000)
정규화LayerNorm, Post-normLayerNorm, Pre-normRMSNorm, Pre-norm
FFNReLU, 4dGELU, 4dSwiGLU, 3.5d (14,336)
어텐션MHAMHAGQA (KV 헤드 8)
편향(bias)있음있음없음

층과 토큰의 격자: 정보는 위로, 그리고 앞에서 뒤로

Transformer의 계산을 한 장의 그림으로 압축하면 격자가 된다. 가로축은 토큰 위치, 세로축은 층이다. 각 토큰 위치에는 아래에서 위로 관통하는 벡터 하나의 "기둥"이 있고(이것이 그 토큰의 잔차 스트림이다), 층마다 두 종류의 일이 일어난다.

맨 위 층, 맨 마지막 토큰의 벡터가 다음 토큰 예측에 쓰인다. 그 벡터에는 층을 거듭하며 앞쪽 모든 토큰에서 끌어온 정보가 쌓여 있다. 아래 시뮬레이터는 교육용으로 만든 작은 예시다. 6개 토큰에 8차원 랜덤 임베딩과 층마다 다른 랜덤 Q·K·V 가중치를 주고 실제로 인과적 어텐션을 계산해, 그 가중치를 연결선의 밝기로 그린다.

3D

Transformer 스택 3D: 잔차 기둥과 인과적 어텐션

드래그 회전 · 휠 확대 · 노드 클릭
노드(구슬)를 클릭하면 그 층·토큰이 어느 앞쪽 토큰을 얼마나 보았는지 표시된다.
층 × 토큰—
어텐션 연결 수—
지금 빛나는 층—
해볼 것: ① 맨 왼쪽 토큰 '오늘'의 노드를 클릭해 보자. 인과적 마스크 때문에 자기 자신만 볼 수 있어 가중치가 항상 1.00이다. ② 맨 오른쪽 토큰은 앞의 6개 모두를 본다. 층마다 가장 많이 보는 토큰이 달라지는지 확인하자(층마다 가중치가 다르기 때문이다). ③ 층 수를 12로 늘리고 '모든 층 연결선 항상 표시'를 켜면, 마지막 토큰의 꼭대기 노드까지 이어지는 경로가 얼마나 많은지 보인다. 연결 수는 층마다 n(n+1)/2 = 21개다.

이 그림에서 두 가지를 기억해 두자. 첫째, 깊이는 "정보를 몇 번 옮기고 가공할 수 있는가"이다. 토큰 A의 정보가 토큰 B로 옮겨진 뒤 그것이 다시 C로 옮겨지려면 최소 두 층이 필요하다. 둘째, 어텐션의 연결선은 층마다 n(n+1)/2개로 시퀀스 길이의 제곱에 비례한다. 긴 컨텍스트가 비싼 이유이자 8장의 KV 캐시가 필요한 이유다.

위치 정보가 필요한 이유: 어텐션은 순서를 모른다

5장의 셀프 어텐션 수식을 다시 보자. 입력 행렬 \(X\)(행 = 토큰)에서 \(Q = XW_Q,\ K = XW_K,\ V = XW_V\)를 만들고

$$\mathrm{Attn}(X) = \mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right)V$$

를 계산한다. 이 식 어디에도 "몇 번째 토큰"이라는 정보가 없다. 토큰 순서를 섞는 것은 \(X\)의 행을 치환 행렬 \(P\)로 섞는 것(\(X \to PX\))인데, 그러면 \(QK^\top \to P\,QK^\top P^\top\)이 되고, 행별 softmax는 행·열을 같이 섞어도 값의 묶음이 그대로이므로

$$\mathrm{Attn}(PX) = P\,\mathrm{Attn}(X)$$
즉 입력 순서를 섞으면 출력도 똑같이 섞일 뿐, 각 토큰의 출력 값 자체는 변하지 않는다. 이런 성질을 순열 등변성(permutation equivariance)이라 한다.

FFN은 토큰마다 따로 작동하니 역시 순서를 모른다. 결국 위치 정보가 없으면 Transformer에게 "개가 사람을 물었다"와 "사람이 개를 물었다"는 같은 단어 주머니(bag of words)다. 그래서 입력이나 어텐션 계산에 위치 인코딩(positional encoding)을 넣어야 한다. 아래 시뮬레이터로 직접 확인해 보자.

SIMULATOR

순서 섞기: 위치 인코딩이 없으면 출력은 따라 섞일 뿐

현재 순서—
토큰별 출력 변화 max |Δ|—
판정—
토큰 칩을 좌우로 끌어 순서를 바꿀 수 있다. 각 열의 위 줄은 지금 순서에서 그 토큰의 어텐션 출력(6차원), 아래 줄은 원래 순서에서 같은 토큰의 출력이다. 해볼 것: ① PE 없이 여러 번 섞어 보자. 각 토큰의 출력 칸 색이 원래와 완전히 같고 |Δ|는 부동소수점 오차(10−16 수준)뿐이다. ② PE를 켜면 같은 토큰이라도 자리가 바뀌면 출력이 달라진다. ③ PE 없이 인과적 마스크만 켜도 출력이 달라진다. 마스크 자체가 "앞에 몇 개가 있는가"라는 위치 단서를 주기 때문이다(아래 심화 상자).
인과적 마스크는 위치 단서다

decoder-only 모델은 인과적 마스크 때문에 순서를 섞으면 각 토큰이 보는 집합 자체가 바뀐다. 첫 토큰은 자기만 보고, 열 번째 토큰은 열 개를 평균한다. 그래서 위치 인코딩을 아예 빼도(NoPE) 모델이 암묵적으로 위치를 어느 정도 추정할 수 있다는 연구가 있다(2023년). 그래도 실전 LLM은 거의 모두 명시적인 위치 정보(현재는 대부분 RoPE)를 쓴다. 상대 거리를 정확히 다루는 쪽이 훨씬 유리하기 때문이다.

사인파 위치 인코딩

원 논문은 위치 p마다 고정된 d차원 벡터를 만들어 토큰 임베딩에 더했다. 차원 쌍 (2i, 2i+1)마다 주파수가 다른 사인·코사인을 쓴다.

$$PE_{(p,\,2i)} = \sin\!\left(\frac{p}{10000^{2i/d}}\right),\qquad PE_{(p,\,2i+1)} = \cos\!\left(\frac{p}{10000^{2i/d}}\right)$$
여기서 p는 위치(0, 1, 2, …), i = 0 … d/2−1은 차원 쌍 번호. 낮은 차원은 파장 2π ≈ 6.3 위치로 빠르게, 높은 차원은 파장 최대 2π·10000 위치로 아주 느리게 돈다.

이진수 카운터를 떠올리면 직관이 생긴다. 1의 자리 비트는 매번 바뀌고, 2의 자리는 두 번에 한 번, 4의 자리는 네 번에 한 번 바뀐다. 사인파 PE는 이것의 연속판이다. 빠른 차원은 가까운 위치를 구별하고, 느린 차원은 먼 위치를 구별한다. 게다가 각 쌍은 위치가 k만큼 이동할 때 각도 kωi만큼 회전하는 2D 벡터이므로, PE(p+k)는 PE(p)의 선형 변환(회전)으로 쓸 수 있다. 저자들은 이 성질 덕분에 모델이 상대 위치를 쉽게 배울 것이라 기대했다.

두 위치의 PE 내적을 계산하면 이 회전 구조가 그대로 드러난다. \(\sin a\sin b + \cos a\cos b = \cos(a-b)\)이므로

$$PE(p)\cdot PE(p+\Delta) = \sum_{i=0}^{d/2-1}\cos(\Delta\,\omega_i),\qquad \omega_i = 10000^{-2i/d}$$
내적은 절대 위치 p와 무관하게 거리 Δ에만 의존하고, Δ = 0에서 최댓값 d/2를 가진 뒤 Δ가 커지면 대체로 줄어든다.
SIMULATOR

PE 히트맵과 거리별 내적

선택 위치 p₁—
거리 |p₁−p₀|—
PE(p₀)·PE(p₁)—
가장 느린 쌍의 파장—
위 히트맵: 세로 = 위치, 가로 = 차원(보라 +1, 분홍 −1). 히트맵을 클릭하거나 끌어 두 번째 위치 p₁을 고른다. 아래 그래프: p₀ 기준으로 거리 Δ에 따른 PE 내적. 해볼 것: ① p₀를 움직여도 아래 곡선 모양이 전혀 변하지 않는다. 내적이 거리에만 의존하기 때문이다. ② d를 8로 줄이면 곡선이 들쭉날쭉해지고 먼 위치끼리도 내적이 커지는 "가짜 이웃"이 생긴다. ③ 히트맵 오른쪽 열(느린 차원)은 최대 길이 512에서도 거의 변하지 않는다. 이 차원들이 긴 범위의 위치를 구별한다.

GPT-2·GPT-3는 사인파 대신 위치마다 벡터 하나를 학습하는 학습된 위치 임베딩을 썼다(GPT-2: 1024 × 768 = 약 79만 개 파라미터). 단점은 학습 때 본 최대 길이를 넘는 위치에 대한 벡터가 아예 없다는 것이다. 그리고 두 방식 모두 위치 정보를 입력에 더해서 섞어 버리므로, 내용 정보와 위치 정보가 같은 벡터 안에서 간섭한다. 이 두 문제를 동시에 해결한 것이 다음 절의 RoPE다.

RoPE: 위치만큼 회전시킨다

RoPE(Rotary Position Embedding, Su et al. 2021)는 위치 정보를 입력에 더하지 않고, 어텐션 직전에 쿼리와 키 벡터를 회전시킨다. d차원 q를 2차원 쌍 d/2개로 나누고, i번째 쌍을 위치 m에 비례한 각도 \(m\theta_i\)만큼 돌린다.

$$\begin{pmatrix} q'_{2i} \\ q'_{2i+1} \end{pmatrix} = \begin{pmatrix} \cos m\theta_i & -\sin m\theta_i \\ \sin m\theta_i & \cos m\theta_i \end{pmatrix}\begin{pmatrix} q_{2i} \\ q_{2i+1} \end{pmatrix},\qquad \theta_i = b^{-2i/d}$$
b는 base(원 논문·Llama 2는 10,000, Llama 3는 500,000). 회전을 \(R(m\theta)\)로 쓰면 \(q_m = R(m\theta)\,q\), \(k_n = R(n\theta)\,k\).

핵심은 두 회전된 벡터의 내적이다. 회전 행렬은 직교 행렬이므로 \(R(a)^\top = R(-a)\), \(R(a)R(b) = R(a+b)\)이다. 따라서

$$q_m^\top k_n = \big(R(m\theta)q\big)^\top R(n\theta)k = q^\top R\big((n-m)\theta\big)\,k$$
어텐션 점수가 절대 위치 m, n이 아니라 상대 거리 m − n에만 의존한다. 두 토큰을 함께 같은 거리만큼 옮겨도 점수는 그대로다. 벡터의 길이는 회전으로 변하지 않으므로 내용 정보(크기)는 보존된다.
한 쌍의 평면에서 q q_m k_n mθ (n−m)θ 내적은 두 화살표 사이 각에만 의존 d/2개의 쌍 = 속도가 다른 시계바늘 (위치 m = 3) i=0, θ=13 rad i=8, θ≈0.320.95 rad i=32, θ=0.010.03 rad i=63≈0.0004 rad head_dim 128 → 쌍 64개 중 4개, base 10,000 기준
그림 6-3. RoPE. 왼쪽: 한 차원 쌍 평면에서 q는 mθ, k는 nθ만큼 돌아가므로 둘 사이 각(따라서 내적)은 (n−m)θ와 원래 각에만 달려 있다. 오른쪽: head_dim 128이면 쌍이 64개이고, 쌍마다 회전 속도가 \(\theta_i = 10000^{-2i/128}\)로 기하급수적으로 느려진다. 빠른 바늘은 가까운 거리를, 느린 바늘은 먼 거리를 구별한다.
SIMULATOR

RoPE 회전: q·k는 m − n에만 의존한다

base b
θi (rad/위치)—
m − n—
q·k (회전 전)—
qm·kn (회전 후)—
위: 점선 화살표 q, k의 끝을 끌어 원래 벡터를 바꾼다. 실선은 위치만큼 회전된 qm, kn. 아래: q = k = (1,1,…,1)일 때 64개 쌍을 모두 합친 점수 \(\frac{1}{64}\sum_i \cos(\Delta\theta_i)\)를 거리 Δ에 대해 그린 장거리 감쇠 곡선(점선은 선택한 쌍 하나). 해볼 것: ① 'm, n 함께 +1'을 여러 번 눌러 보자. 두 화살표가 함께 돌아가며 qm·kn이 소수점 끝까지 변하지 않는다. ② i를 0에서 40으로 올리면 회전이 거의 멈춘다. 이런 느린 쌍은 사실상 위치와 무관한 "내용" 채널이다. ③ base를 500,000으로 바꾸면 감쇠 곡선이 훨씬 천천히 떨어진다. 긴 컨텍스트(Llama 3는 8K, 3.1은 128K)를 위해 base를 키우는 이유다.

실제 구현에서는 회전 행렬을 곱하지 않는다. 각 쌍에 \(\cos m\theta_i\), \(\sin m\theta_i\)를 원소별로 곱해 더하는 \(O(d)\) 연산이면 충분하고, cos/sin 표는 미리 계산해 둔다. RoPE는 Q와 K에만 적용하고 V에는 적용하지 않는다. 위치는 "누구를 볼지"(점수)에만 영향을 주고, 가져오는 내용(값)은 그대로 둔다. 학습 길이보다 긴 문맥으로 늘릴 때는 θ를 줄여 회전을 느리게 만드는 위치 보간(PI), NTK-aware 스케일링, YaRN 같은 기법을 쓴다.

잔차 스트림: 정보 고속도로

블록의 출력을 다음 블록에 그대로 넘기는 대신, Transformer는 블록이 계산한 값을 입력에 더한다.

$$x_{l+1} = x_l + F_l\big(\mathrm{Norm}(x_l)\big)$$
\(F_l\)은 l번째 하위 층(어텐션 또는 FFN). \(x_l\)이 층을 관통하는 d차원 잔차 스트림(residual stream)이다.

이렇게 보면 각 블록은 스트림을 통째로 바꾸는 것이 아니라, 스트림에서 필요한 것을 읽어(정규화 → 계산) 작은 수정 \(\Delta = F_l(\cdot)\)을 써 넣는 장치다. 마지막 층의 벡터는 임베딩 + 모든 블록 출력의 합 \(x_L = x_0 + \sum_l \Delta_l\)이다. 해석 가능성 연구에서는 이 스트림을 여러 블록이 공유하는 "통신 버스"로 본다. 앞 층의 어텐션 헤드가 써 놓은 정보를 몇 층 뒤의 FFN이 읽어 가는 식이다.

학습 측면의 이점은 3장의 기울기 소실과 직결된다. 연쇄법칙으로 \(\partial x_L/\partial x_0\)를 계산하면, 잔차가 없을 때는 야코비안의 곱 \(\prod_l J_l\)이라 층마다 1보다 작은 배율이 곱해져 기하급수적으로 사라진다. 잔차가 있으면

$$\frac{\partial x_L}{\partial x_0} = \prod_{l}\big(I + J_l\big) = I + \sum_l J_l + \cdots$$
항등 행렬 \(I\)가 늘 남는다. 출력의 기울기가 덧셈 경로를 타고 첫 층까지 그대로 도달하는 "직통로"가 생긴다. ResNet(2015)이 152층을 학습시킨 비결이 이것이다.
SIMULATOR

잔차 스트림: 더하느냐, 덮어쓰느냐

‖xL‖ / ‖x0‖—
cos(xL, x0)—
기울기 ‖∂xL/∂x0‖—
블록은 \(F(x) = W_2\,\mathrm{ReLU}(W_1 x)\)(d → 2d → d, 랜덤 가중치 표준편차 g/√fan_in)이고, 잔차 모드에서는 GPT-2 초기화처럼 W₂를 1/√L로 줄인다. 위: 층마다 d차원 벡터를 열 하나로 그린 스트림(보라 +, 분홍 −). 열을 클릭하면 그 층의 xl(막대), Δl(분홍 테두리), xl+1을 크게 보여 준다. 아래: 층별 ‖xl‖과 기울기 노름(로그 축, 실선 = 현재 모드, 점선 = 반대 모드). 해볼 것: ① 잔차를 끄면 ‖x‖와 기울기가 층마다 약 0.7배씩 줄어 12층 뒤 1% 아래로 붕괴한다. ② 잔차를 끈 채 g를 1.4 이상으로 올리면 이번엔 폭주한다. 층당 분산 배율 g⁴/2가 정확히 1이 되는 g ≈ 1.19 근처에서만 살아남는 "칼날 위" 상태다. ③ 잔차를 켜면 L = 32에서도 cos(xL, x0)가 높게 유지되고 기울기가 1 근처에 머문다. 입력 정보가 고속도로를 타고 끝까지 간다.

정규화: LayerNorm과 RMSNorm

잔차 스트림에는 층마다 값이 더해지므로 크기가 점점 커지거나 특정 차원이 튈 수 있다. 하위 층에 넣기 전에 벡터의 스케일을 일정하게 맞춰 주는 것이 정규화다. Transformer는 배치가 아니라 토큰 하나의 d개 원소에 대해 통계를 낸다. 원 논문과 GPT-2는 LayerNorm을 쓴다.

$$\mathrm{LN}(x) = \gamma \odot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta,\qquad \mu = \frac{1}{d}\sum_j x_j,\quad \sigma^2 = \frac{1}{d}\sum_j (x_j-\mu)^2$$
γ, β는 학습되는 d차원 스케일·이동 파라미터. 평균을 빼서 중심을 맞추고 표준편차로 나눠 크기를 맞춘다.

Llama, Mistral, Gemma 등 최근 모델 대부분은 평균 빼기를 생략한 RMSNorm(Zhang & Sennrich, 2019)을 쓴다.

$$\mathrm{RMSNorm}(x) = \gamma \odot \frac{x}{\mathrm{RMS}(x)},\qquad \mathrm{RMS}(x) = \sqrt{\frac{1}{d}\sum_j x_j^2 + \epsilon}$$
β도 없다. 파라미터가 절반(d개)이고, 평균 계산·빼기가 없어 조금 빠르다. 실험적으로 성능 차이는 거의 없다. "중심 맞추기보다 크기 맞추기가 중요하다"는 관찰에서 나왔다.
SIMULATOR

정규화: 같은 입력, 두 가지 결과

입력 평균 μ—
입력 표준편차 σ—
입력 RMS—
두 출력의 차 max—
왼쪽 입력 막대(8차원)를 위아래로 끌어 값을 바꾼다. 점선은 평균 μ, 회색 띠는 ±RMS. 해볼 것: ① '모든 값 ×2'를 누르면 두 출력 모두 전혀 변하지 않는다(스케일 불변). ② '모든 값 +1'을 누르면 LayerNorm 출력은 그대로지만(평균을 빼므로 이동 불변) RMSNorm 출력은 달라진다. ③ 입력 평균을 0 근처로 맞추면 두 출력이 거의 같아진다. 실제 잔차 스트림은 평균이 0에 가까워 RMSNorm으로 충분하다. ④ '이상치 하나'를 누르면 큰 값 하나가 RMS를 키워 나머지 값들이 모두 0 쪽으로 눌린다. 9장의 이상치 문제와 연결된다.

Pre-norm과 Post-norm

정규화를 어디에 두느냐도 중요하다. 원 논문은 잔차를 더한 뒤에 정규화했다(Post-norm). GPT-2 이후 대부분은 하위 층에 들어가기 전에 정규화한다(Pre-norm).

Post-norm (원 논문, 2017) Pre-norm (GPT-2 이후 표준) x F (Attn/FFN) + LN x' = LN(x + F(x)) 잔차 경로 위에 LN → 깊으면 학습 초기 불안정, warm-up 필수 x Norm F (Attn/FFN) + x' = x + F(Norm(x)) 잔차 경로가 깨끗한 덧셈뿐 → 기울기 직통, 깊어도 안정
그림 6-4. Post-norm은 잔차를 더한 결과 전체를 정규화하므로 스트림 위에 LN이 끼어 있다. Pre-norm은 하위 층의 입력만 정규화하고 스트림 자체는 덧셈만 거친다. Pre-norm에서는 스트림 크기가 층마다 커지므로 마지막에 최종 Norm을 한 번 더 둔다(그림 6-2).

Post-norm은 잘 학습되면 성능이 약간 좋다는 보고도 있지만, 층이 깊어지면 출력 쪽 층의 기울기가 커져 학습 초기에 발산하기 쉽고 학습률 warm-up에 민감하다. Pre-norm은 위 잔차 시뮬레이터의 "직통로"가 정규화에 의해 끊기지 않으므로 수십~백여 층에서도 안정적이다. 그래서 GPT-2, GPT-3, Llama 등 거의 모든 대형 LLM이 Pre-norm을 쓴다. 최근에는 하위 층 출력에도 정규화를 한 번 더 거는 변형(Gemma 2의 "sandwich norm" 등)도 쓰인다.

FFN: 토큰마다 독립적인 2층 MLP

어텐션이 토큰 사이에 정보를 옮긴다면, FFN(Feed-Forward Network, MLP)은 각 토큰의 벡터를 그 자리에서 가공한다. 구조는 2장의 2층 MLP 그대로다. d차원을 4d로 넓혔다가 비선형 함수를 거쳐 다시 d로 줄인다.

$$\mathrm{FFN}(x) = W_2\,\phi(W_1 x + b_1) + b_2,\qquad W_1 \in \mathbb{R}^{4d\times d},\ W_2 \in \mathbb{R}^{d\times 4d}$$
φ는 ReLU(원 논문)나 GELU(GPT-2). 파라미터 수는 2 · d · 4d = 8d². 어텐션의 Q, K, V, O 네 행렬(4d²)의 두 배다.

그래서 MHA를 쓰는 표준 블록에서 FFN이 블록 파라미터의 약 2/3(8d² / 12d²)를 차지한다. GQA로 K, V가 작아진 Llama 3 8B에서는 비중이 더 커져, 층당 어텐션 4,194만 개 대 FFN 1억 7,616만 개로 약 81%다. 모델이 배운 "지식"의 상당 부분이 FFN에 들어 있다고 보는 이유다.

키-값 메모리 해석

FFN을 다르게 읽어 보자. \(W_1\)의 각 행 \(k_j\)를 "키", \(W_2\)의 각 열 \(v_j\)를 "값"이라 하면

$$\mathrm{FFN}(x) = \sum_{j=1}^{4d} \phi(k_j \cdot x)\, v_j$$
입력이 키 \(k_j\)와 잘 맞으면(내적이 크면) 뉴런 j가 켜지고, 그 값 벡터 \(v_j\)가 잔차 스트림에 더해진다. 어텐션과 비슷하지만 키와 값이 입력이 아니라 학습된 가중치이고, softmax 대신 φ를 쓴다.

Geva et al.(2021)은 실제로 FFN 뉴런 상당수가 특정 패턴("~로 끝나는 문장", "TV 프로그램 이름" 등)에 반응하고, 그 값 벡터가 다음 토큰 분포를 특정 단어 쪽으로 미는 것을 보였다. 사실 관계를 편집하는 연구(ROME 등)가 FFN 가중치를 고치는 것도 이 해석에 기반한다.

SwiGLU: 게이트가 달린 FFN

PaLM, Llama 이후의 모델은 SwiGLU(Shazeer, 2020)를 쓴다. 행렬을 세 개 두고, 하나는 SiLU를 거친 "게이트", 하나는 그대로의 "업" 경로로 만들어 원소별로 곱한다.

$$\mathrm{SwiGLU}(x) = W_2\Big(\mathrm{SiLU}(W_1 x) \odot W_3 x\Big),\qquad \mathrm{SiLU}(z) = z\,\sigma(z)$$
행렬이 3개이므로 같은 파라미터 수를 맞추려면 중간 차원을 \(\tfrac{2}{3}\cdot 4d = \tfrac{8}{3}d\)로 줄인다(3 · d · 8d/3 = 8d²). Llama 3 8B는 d = 4096에 dff = 14,336(= 3.5d)으로 조금 더 크게 잡았다.
표준 FFN (GPT-2) SwiGLU FFN (Llama) x (d) W₁ d→4d GELU 4d 뉴런 W₂ 4d→d out 8d² 파라미터 x W₁ gate W₃ up SiLU ⊙ W₂ down out 3·d·d_ff d_ff ≈ 8d/3 ~ 3.5d
그림 6-5. 표준 FFN은 넓히고(W₁) 비선형을 거쳐 좁힌다(W₂). SwiGLU는 넓히는 경로를 둘로 나눠, 한쪽(SiLU 게이트)이 다른 쪽(업)의 각 원소를 얼마나 통과시킬지 곱셈으로 조절한다. 같은 파라미터에서 언어 모델 손실이 일관되게 조금 더 낮다.

MoE: 전문가 중 몇 명만 깨운다

FFN이 지식을 담는다면, FFN을 키우면 모델이 더 많이 안다. 하지만 FFN을 키우면 토큰마다 연산도 그만큼 는다. MoE(Mixture of Experts)는 FFN을 E개의 "전문가"로 복제하고, 토큰마다 라우터가 그중 k개만 골라 계산한다. 파라미터(지식 용량)는 E배로 늘지만 토큰당 연산은 k배만 는다.

$$h = W_r\,x \in \mathbb{R}^{E},\qquad \mathcal{T} = \mathrm{top}\text{-}k(h),\qquad y = \sum_{i\in\mathcal{T}} \frac{e^{h_i}}{\sum_{j\in\mathcal{T}} e^{h_j}}\;\mathrm{FFN}_i(x)$$
\(W_r\)은 d × E 라우터 행렬(파라미터가 아주 적다). 선택된 k개 전문가의 로짓만 softmax해 가중합한다(Mixtral 방식). Switch Transformer(2021)는 k = 1, Mixtral 8x7B(2023)는 E = 8, k = 2.
토큰 x 라우터W_r: d×E E1 FFN E2 FFN · 0.62 E3 FFN E4 FFN E5 FFN E6 FFN · 0.38 E7 FFN E8 FFN Σ y 전체 파라미터: 8개 전문가 토큰당 계산: 2개만 Mixtral 8x7B 전체 ≈ 46.7B 활성 ≈ 12.9B
그림 6-6. MoE 층. 어텐션은 그대로 두고 FFN 자리만 E개의 전문가로 바꾼다. 라우터가 토큰마다 로짓을 내고 상위 k개(여기서는 E2, E6)의 FFN만 계산해 가중합한다. 이름은 "8x7B"지만 어텐션·임베딩은 공유되므로 전체는 56B가 아니라 약 47B다.

MoE의 어려움은 부하 불균형이다. 라우터가 몇몇 전문가만 편애하면 그 전문가는 과로하고 나머지는 놀며, 학습이 진행될수록 편애가 강화된다. 또 병렬 하드웨어에서는 전문가마다 처리할 토큰 수(용량(capacity))를 미리 정해 두는데, 넘치는 토큰은 그 전문가에서 드롭된다(그 토큰은 잔차 연결만 통과한다). 그래서 부하 균형 보조 손실 \(\mathcal{L}_{aux} = E\sum_i f_i P_i\)(fi: 전문가 i로 간 토큰 비율, Pi: 라우터 확률 평균)을 더해 고르게 나누도록 유도한다. 균등하면 1, 쏠릴수록 커진다.

SIMULATOR

MoE 라우팅: top-k, 용량, 드롭

top-k
전문가당 용량—
드롭된 배정—
최대 부하 / 평균—
보조 손실 Laux—
위: 토큰 12개(행) × 전문가 8개(열)의 라우터 로짓. 칸 색은 softmax 확률, 숫자는 로짓이다. 칸을 위아래로 끌면 로짓이 바뀐다. 테두리 칸이 선택된 top-k이고, ✕는 용량 초과로 드롭된 배정이다(토큰은 위에서부터 순서대로 배정). 아래: 전문가별 부하와 용량선. 해볼 것: ① 편향을 3으로 올리면 거의 모든 토큰이 전문가 1로 몰려 드롭이 폭증하고 Laux가 커진다. ② 편향 0에서 용량 계수를 1.0 → 2.0으로 올리면 드롭이 사라지지만, 그만큼 빈 슬롯(패딩 연산)이 늘어난다. ③ 한 행의 로짓 하나를 크게 끌어올려 그 토큰의 라우팅을 직접 바꿔 보자.

MoE는 같은 연산량으로 더 큰 모델을 만드는 가장 실용적인 방법이 되어, 2024~2025년의 대형 공개 모델(DeepSeek-V3: 전체 약 671B / 활성 약 37B, 전문가 256개 중 8개 + 공유 전문가 1개; Llama 4, Qwen3 MoE 등) 상당수가 MoE다. 대신 메모리는 전체 파라미터만큼 필요하다. Mixtral 8x7B는 토큰당 13B 모델만큼 계산하지만, BF16 가중치만 약 94 GB를 올려 둬야 한다.

파라미터와 FLOPs 직접 세기

이제 블록의 모든 부품을 알았으니 모델 크기를 정확히 셀 수 있다. Llama 계열(편향 없음, RMSNorm, SwiGLU, GQA)에서 head_dim \(d_h = d/H\), KV 헤드 수 \(H_{kv}\)라 하면 한 층의 파라미터는

$$\underbrace{2d^2 + 2\,d\,H_{kv} d_h}_{W_Q,\,W_O,\,W_K,\,W_V} \;+\; \underbrace{3\,d\,d_{ff}}_{\text{SwiGLU}} \;+\; \underbrace{2d}_{\text{RMSNorm}\times 2}$$
여기에 임베딩 V·d, (가중치를 묶지 않았다면) LM head V·d, 최종 Norm d를 더한다. MHA(\(H_{kv}=H\)), 4d FFN이면 층당 약 12d²이고, 따라서 \(N \approx 12\,L\,d^2\)라는 유명한 근사가 나온다.

Llama 3 8B로 검산해 보자. d = 4096, L = 32, H = 32, Hkv = 8, dh = 128, dff = 14,336, V = 128,256, 임베딩과 LM head는 따로다.

항목식개수
WQ, WO2 × 4096 × 409633,554,432
WK, WV2 × 4096 × (8 × 128)8,388,608
SwiGLU3 × 4096 × 14336176,160,768
RMSNorm 2개2 × 40968,192
층당 합계218,112,000
× 32층6,979,584,000
임베딩 + LM head2 × 128256 × 40961,050,673,152
최종 RMSNorm40964,096
총계8,030,261,248 ≈ 8.03B

연산량은 더 간단하다. 행렬-벡터 곱에서 가중치 하나당 곱하기 1번 + 더하기 1번이므로, 토큰 하나의 순전파는 약 2N FLOPs다(임베딩 조회는 곱셈이 없으니 엄밀히는 임베딩을 뺀 N이지만 LM head는 포함한다). 여기에 어텐션 점수 계산 \(QK^\top\)과 가중합이 컨텍스트 길이 n에 비례해 층당 약 \(4\,n\,d\) FLOPs(\(QK^\top\)와 가중합이 각각 \(2nd\)) 더해진다. 학습은 역전파까지 순전파의 약 3배, 즉 토큰당 약 6N FLOPs다(7장의 스케일링 법칙). 가중치 메모리는 BF16이면 파라미터당 2바이트다.

SIMULATOR

파라미터·FLOPs 계산기

프리셋
총 파라미터—
활성 파라미터 (토큰당)—
공개 값과 비교—
토큰당 순전파 FLOPs—
BF16 가중치 메모리—
FFN 비중 (층 내부)—
막대는 부품별 파라미터 수(전체 대비 %)이고, 아래 줄은 토큰당 FLOPs 분해(2 × 활성 비임베딩 파라미터 + 어텐션 점수 4·L·n·d)다. 해볼 것: ① 프리셋을 차례로 눌러 공개 값(GPT-2 S 124M, XL 1.56B, Llama 3 8B 8.03B, 70B 70.6B, Mixtral 46.7B/12.9B)과 비교하자. ② GPT-2 S에서 가중치 공유를 끄면 LM head 3,860만 개가 늘어난다. 작은 모델일수록 어휘 행렬 비중이 크다. ③ Llama 3 8B에서 KV 헤드를 32로 올리면(MHA) 층당 2,500만 개, 전체 약 8억 개가 늘어난다. ④ 컨텍스트 n을 128K(131072)로 키우면 어텐션 FLOPs가 2N에 맞먹기 시작한다.
암산용 규칙

파라미터 N ≈ 12·L·d² (MHA·4d FFN 기준), 추론 FLOPs ≈ 2N/토큰, 학습 FLOPs ≈ 6N/토큰, BF16 가중치 ≈ 2N 바이트. 70B 모델은 가중치만 140 GB라 H100 80 GB 한 장에 올라가지 않는다. 그래서 텐서 병렬로 여러 장에 나누거나 9장의 양자화로 줄인다.

핵심 정리

  1. 오늘날 LLM은 거의 모두 decoder-only Transformer다. 임베딩 → N × [Norm → 인과적 어텐션 → +잔차 → Norm → FFN → +잔차] → 최종 Norm → LM head → softmax.
  2. 어텐션과 FFN은 순서에 무관(순열 등변)하다. 위치 정보는 입력에 더하는 사인파/학습 PE, 또는 Q·K를 회전시키는 RoPE로 넣는다.
  3. RoPE는 차원 쌍을 위치 × θi만큼 회전시켜 \(q_m^\top k_n = q^\top R((n-m)\theta)k\)가 되게 한다. 점수가 상대 거리에만 의존하고, base를 키우면 긴 컨텍스트로 확장된다.
  4. 잔차 스트림은 층을 관통하는 정보 고속도로다. 각 블록은 읽고 더할 뿐이며, 항등 경로 덕분에 기울기가 깊은 층까지 소실되지 않는다. Pre-norm은 이 경로를 깨끗하게 유지한다.
  5. LayerNorm은 평균을 빼고 표준편차로 나누며, RMSNorm은 RMS로만 나눈다(파라미터 절반, 이동 불변성 없음). 둘 다 스케일 불변이다.
  6. FFN(또는 SwiGLU)은 블록 파라미터의 2/3 이상을 차지하는 키-값 메모리다. MoE는 FFN을 E개로 복제해 top-k만 계산하므로 전체 파라미터와 활성 파라미터가 갈라진다.
  7. Llama 계열 층당 파라미터는 2d² + 2·d·Hkv·dh + 3·d·dff + 2d. 추론 ≈ 2N FLOPs/토큰, BF16 가중치 ≈ 2N 바이트.

확인 퀴즈

1. 위치 정보가 전혀 없는 양방향 셀프 어텐션에 "개가 사람을 물었다"와 "사람이 개를 물었다"를 넣었다. 두 입력에서 토큰 "개"의 출력 벡터는?

Attn(PX) = P·Attn(X)이다. 순서를 섞으면 출력 행도 똑같이 섞일 뿐, 각 토큰의 출력은 그대로다. 그래서 위치 인코딩이 필요하다(인과적 마스크가 있다면 보는 집합이 바뀌어 달라진다).

2. RoPE에서 쿼리를 위치 m, 키를 위치 n에서 회전시킨 뒤 내적한다. m = 10, n = 7일 때와 같은 점수를 주는 경우는?

\(q_m^\top k_n = q^\top R((n-m)\theta)k\)이므로 m − n이 같으면(= 3) 점수가 같다. m = 7, n = 10은 m − n = −3이라 회전 방향이 반대고, 20/14는 거리 6이다.

3. 잔차 연결이 깊은 네트워크 학습을 돕는 가장 직접적인 이유는?

잔차가 없으면 기울기는 ∏Jl로 층마다 곱해져 소실·폭주하기 쉽다. x + F(x)의 미분은 I + J라 항등 성분이 남는다.

4. 입력 벡터의 모든 원소에 같은 상수 c를 더했다. 출력이 변하지 않는 정규화는? (γ = 1, β = 0, ε 무시)

LayerNorm은 평균을 빼므로 이동 불변이다. RMSNorm은 평균을 빼지 않아 c가 RMS와 방향을 모두 바꾼다. 반면 상수배(스케일)에는 둘 다 불변이다.

5. d = 4096인 Llama식 블록에서 SwiGLU FFN의 dff = 14,336이다. FFN 파라미터는?

SwiGLU는 gate(W₁), up(W₃), down(W₂) 세 행렬을 쓰므로 3·d·dff = 176,160,768개다. 층당 어텐션(약 4,194만 개)의 4배가 넘는다.

6. Mixtral 8x7B(전문가 8개, top-2)에 대한 설명으로 옳은 것은?

어텐션·임베딩은 공유되고 FFN만 8개라 전체는 약 46.7B다. 토큰마다 전문가 2개만 계산하므로 활성 파라미터는 약 12.9B다. 하지만 어떤 토큰이 어느 전문가로 갈지 모르니 가중치는 전부 메모리에 있어야 한다.