Chapter 05

어텐션

'배'라는 글자 하나만 보고는 그것이 과일인지, 바다 위의 배인지, 사람의 배인지 알 수 없다. 단어의 뜻은 주변 단어가 정한다. 어텐션(attention)은 문장 속 모든 토큰이 다른 모든 토큰을 둘러보고, 지금 자신에게 중요한 정보를 골라 섞어 오는 연산이다. 2017년 Transformer 이후 거의 모든 언어 모델·이미지 모델·음성 모델의 심장이 된 이 연산을, 이 장에서는 숫자 하나하나까지 손으로 만져 본다. 점수 행렬의 칸을 끌어 올리면 소프트맥스가 다시 계산되고, 쿼리 벡터를 끌어 옮기면 어텐션 패턴이 통째로 바뀐다.

왜 어텐션인가: 뜻은 문맥이 정한다

4장에서 각 토큰을 하나의 벡터(임베딩)로 바꿨다. 그런데 임베딩 테이블은 토큰마다 벡터를 딱 하나만 준다. 다음 세 문장의 '배'는 모두 같은 토큰이니 처음에는 같은 벡터에서 출발한다.

시장에서 산 배가 달다  → 과일
항구에 배가 들어왔다  → 선박
점심을 급하게 먹었더니 배가 아프다  → 신체

사람은 '시장·달다', '항구·들어왔다', '먹었더니·아프다'를 보고 즉시 뜻을 고른다. 모델도 그렇게 하려면 각 토큰의 벡터를 주변 토큰의 정보로 고쳐 쓰는 장치가 필요하다. 영어의 고전적인 예도 있다.

The animal didn't cross the street because it was too tired
The animal didn't cross the street because it was too wide

두 문장은 마지막 단어 하나만 다르다. 그런데 'it'이 가리키는 대상이 'animal'에서 'street'로 바뀐다. 'it'의 의미를 정하려면 문장 끝의 'tired/wide'와 문장 앞의 'animal/street'를 동시에 참고해야 한다. 거리가 먼 두 토큰 사이에 정보가 직접 오가야 한다는 뜻이다.

RNN의 병목: 모든 것을 벡터 하나에

Transformer 이전의 대표 구조인 RNN(Recurrent Neural Network)은 토큰을 왼쪽부터 하나씩 읽으며 은닉 상태 \(h_t = f(h_{t-1}, x_t)\)를 갱신했다. 번역용 인코더-디코더 RNN(2014)은 원문 전체를 읽은 마지막 은닉 상태 하나, 즉 고정 길이 벡터 c에 문장 전체를 압축한 뒤 디코더가 그 벡터만 보고 번역문을 만들었다. 문장이 길어질수록 앞부분 정보가 희미해지고, 번역 품질이 문장 길이에 따라 뚝 떨어졌다.

(a) RNN 인코더-디코더: 문장 전체가 벡터 c 하나로 압축된다 h₁ h₂ h₃ ⋯ h₁₁ Theanimaldidn't⋯tired c 고정 길이 병목 s₁ s₂ ⋯ 그동물은⋯ (b) 어텐션: 매 단계 모든 위치를 직접 들여다보고, 가중치로 섞는다 h₁ h₂ h₃ ⋯ h₁₁ Theanimaldidn't⋯tired s₂ '동물은'을 만들 차례 선 굵기 = 어텐션 가중치 거리와 무관하게 한 단계로 연결
그림 5-1. (a) 2014년식 인코더-디코더 RNN은 원문 전체를 마지막 은닉 상태 c에 욱여넣는다. (b) Bahdanau 등(2014)은 디코더가 매 단계 인코더의 모든 은닉 상태를 다시 보고, 지금 필요한 위치에 큰 가중치를 주어 섞어 오게 했다. 이것이 어텐션의 출발점이다. Transformer(Vaswani 등, 2017)는 RNN을 아예 없애고 어텐션만으로 문장을 처리했다.

어텐션의 장점은 세 가지로 정리된다. 첫째, 거리와 무관한 직접 연결: 1번 토큰과 1000번 토큰 사이도 한 번의 연산으로 정보가 오간다(RNN이라면 1000단계를 거쳐야 한다). 둘째, 병렬성: 모든 토큰 쌍의 유사도는 행렬곱 한 번으로 동시에 계산되므로 GPU에 꼭 맞는다. 셋째, 내용 기반 라우팅: 어디를 볼지가 고정된 규칙이 아니라 토큰의 내용에 따라 매번 달라진다. 대가도 있다. 모든 쌍을 보므로 비용이 길이의 제곱에 비례한다(9절).

직관: 부드러운 사전 검색

파이썬 딕셔너리 d["animal"]는 키가 정확히 일치하는 항목 하나의 값을 돌려준다. 어텐션은 이것을 미분 가능하게 '부드럽게' 만든 것이다. 세 가지 역할이 있다.

쿼리를 모든 키와 비교해 유사도 점수 \(s_j = q\cdot k_j\)를 얻고, 소프트맥스로 합이 1인 가중치 \(a_j\)로 바꾼 뒤, 값을 그 가중치로 평균한다. 정확히 하나만 고르는 대신 '조금씩 다' 고르는 셈이다.

$$ a_j = \frac{\exp(q\cdot k_j / T)}{\sum_{l}\exp(q\cdot k_l / T)}, \qquad \text{출력} = \sum_j a_j\, v_j $$
여기서 \(T\)는 온도. \(T\to 0\)이면 가장 비슷한 키 하나만 고르는 hard lookup(딕셔너리)이 되고, \(T\to\infty\)면 모든 값의 단순 평균이 된다.
하드 룩업 (딕셔너리) q = "animal" "street" "animal" "tired" v₁ v₂ v₃ v₂ 일치 = 1, 불일치 = 0 → 미분 불가, 비슷한 키가 있어도 정확히 같지 않으면 실패 소프트 룩업 (어텐션) q k₁ k₂ k₃ 0.42.1−0.3 점수 q·kⱼ .14.78.08 softmax → aⱼ 출력 = 0.14·v₁ + 0.78·v₂ + 0.08·v₃ 비슷한 정도만큼 섞는다 → 매끄럽고 미분 가능
그림 5-2. 딕셔너리는 일치하는 키 하나만 고른다. 어텐션은 모든 키와의 유사도를 소프트맥스로 확률처럼 바꿔 값을 섞는다. 가중치가 연속적이므로 경사하강(3장)으로 "어떤 키를 얼마나 볼지"를 학습할 수 있다.
SIMULATOR

소프트 룩업: 쿼리를 끌어 키 사이를 돌아다니기

q·키 모두 드래그
유사도
최대 가중치—
엔트로피—
유효 키 수 1/Σa²—
출력 색 (RGB)—
해볼 것: ① 쿼리 q(큰 원)를 한 키 바로 위에 올리고 온도를 낮춰 보자. 가중치가 그 키 하나로 몰리며 출력 색이 그 키의 색이 된다 — 딕셔너리다. ② 온도를 5로 올리면 출력이 모든 색의 평균(탁한 회색 쪽)으로 간다. ③ '내적' 모드에서 q를 원점에서 멀리 끌면 같은 방향이라도 길이가 긴 키가 이긴다(내적은 길이에도 비례). '−거리²' 모드와 비교해 보자. 키를 끌어 원점 근처로 모으면 어떤 쿼리에도 점수가 비슷해진다.

이 시뮬레이터의 값 \(v_j\)는 색(RGB 3차원 벡터)이다. 실제 어텐션에서 값은 수십~수백 차원 벡터이고, 출력은 그 벡터들의 가중 평균이다. 원리는 똑같다. 한 가지 짚어 둘 것은 내적은 방향뿐 아니라 길이에도 반응한다는 점이다. 그래서 학습된 모델은 키·쿼리의 길이(노름)를 이용해 "이 토큰은 웬만하면 주목받게" 같은 효과도 낸다.

Scaled dot-product attention

문장에 토큰이 n개 있고 각각 d차원 벡터라고 하자. 이를 행으로 쌓은 것이 입력 행렬 \(X\in\mathbb{R}^{n\times d}\)다. 모든 토큰이 동시에 쿼리이자 키이자 값이므로(셀프 어텐션), 행렬 세 개 \(W_Q, W_K\in\mathbb{R}^{d\times d_k}\), \(W_V\in\mathbb{R}^{d\times d_v}\)로 각 역할의 벡터를 만든다.

$$ Q = XW_Q,\quad K = XW_K,\quad V = XW_V $$ $$ \mathrm{Attention}(Q,K,V) = \underbrace{\mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}} + M\right)}_{A\ (n\times n)}\,V $$
\(S = QK^\top/\sqrt{d_k}\)의 (i, j) 원소는 "토큰 i의 쿼리와 토큰 j의 키의 유사도". 소프트맥스는 행마다 따로 적용되어 각 행의 합이 1이 된다. \(M\)은 마스크(볼 수 없는 위치는 \(-\infty\), 나머지는 0). 출력 \(O = AV\)의 i번째 행은 값 벡터들의 가중 평균이다.
모양으로 따라가 보기 (n = 4 토큰, d = 6, d_k = d_v = 3) X n×d · W_Q d×d_k = Q n×d_k K n×d_k V XW_V → XW_K → Q · Kᵀ d_k×n = S: n×n ÷√d_k, +M 행별 softmax A: n×n 각 행 합 = 1 · V = O n×d_v · W_O → n×d • 비용의 핵심은 n×n 행렬 S, A • 입력과 출력 모양이 같다(n×d) → 층을 계속 쌓을 수 있다 • i번째 출력 행 = Σⱼ Aᵢⱼ·vⱼ
그림 5-3. 셀프 어텐션의 행렬 모양. 입력 X(n×d)에서 Q·K·V를 만들고, n×n 점수 행렬 S를 행별 소프트맥스로 A로 바꾼 뒤, V와 곱해 다시 n×d_v를 얻는다. 멀티헤드(7절)에서는 출력 투영 \(W_O\)가 헤드들을 섞어 다시 n×d로 돌려놓는다.

왜 \(\sqrt{d_k}\)로 나누는가

쿼리와 키의 각 성분이 평균 0, 분산 1인 독립 확률변수라고 하자. 내적 \(q\cdot k = \sum_{i=1}^{d_k} q_i k_i\)는 분산 1짜리 항 \(d_k\)개의 합이므로 분산이 \(d_k\), 표준편차가 \(\sqrt{d_k}\)다. \(d_k = 128\)이면 점수가 대략 ±11 범위로 퍼진다. 소프트맥스는 입력 차이에 지수적으로 반응하므로, 이렇게 큰 점수가 들어가면 가장 큰 항 하나에 가중치가 거의 다 몰린다(포화). 포화된 소프트맥스는 기울기 \(\partial a/\partial s = \mathrm{diag}(a) - aa^\top\)가 거의 0이라 학습 신호가 사라진다. \(\sqrt{d_k}\)로 나누면 분산이 다시 1이 되어 차원이 몇이든 소프트맥스가 '적당히 부드러운' 영역에서 시작한다.

$$ \mathrm{Var}(q\cdot k) = \sum_{i=1}^{d_k}\mathrm{Var}(q_i k_i) = d_k \quad\Longrightarrow\quad \mathrm{Var}\!\left(\frac{q\cdot k}{\sqrt{d_k}}\right) = 1 $$
SIMULATOR

스케일링의 필요성: 차원이 커지면 소프트맥스가 굳는다

Var(q·k) 측정—
Var(q·k/√d) 측정—
평균 최대 가중치 (스케일 X / O)—
평균 엔트로피 (X / O)—
기울기 크기 ‖J‖F (X / O)—
해볼 것: ① d_k를 2에서 1024까지 올리며 'Var(q·k)'가 d_k를 그대로 따라가는지 보자(400회 샘플 평균). ② d_k = 128(Llama·GPT-2급 헤드 차원은 64~128)에서 스케일 없는 소프트맥스(분홍)는 거의 한 칸만 남고, 엔트로피가 0 근처로 떨어진다. ③ 아래 곡선은 d_k별 평균 최대 가중치다. 스케일 O(보라)는 d_k와 무관하게 평평하다. ④ 기울기 크기를 비교해 보자. 포화되면 학습 신호가 수십 배 작아진다. 모든 값은 정규분포 q, k를 실제로 뽑아 계산한다.

어텐션 행렬을 직접 편집하기

이제 이 장의 핵심 실험이다. 실제 모델에서 점수 행렬 S는 \(QK^\top/\sqrt{d_k}\)로 계산되지만, 여기서는 그 앞단을 잠시 잊고 S의 칸을 손으로 직접 정한다. 칸을 위로 끌면 점수가 올라가고, 아래로 끌면 내려간다. 오른쪽(모바일에서는 아래)의 A는 각 행에 소프트맥스를 적용한 실제 결과이고, 맨 아래 아크 그림은 선택한 행(쿼리 토큰)이 어느 토큰에서 정보를 가져오는지를 선 굵기로 보여 준다.

먼저 관찰해 둘 성질 세 가지가 있다. ① 소프트맥스는 행 안에서의 상대 차이에만 반응한다. 한 행 전체에 같은 값을 더해도 A는 그대로다. ② 점수 차이가 1 늘 때마다 가중치 비율은 \(e \approx 2.7\)배가 된다. 차이가 3이면 약 20배, 5면 약 150배다. ③ 다른 행은 서로 독립이다. 행 i를 고쳐도 다른 행의 A는 변하지 않는다. 각 토큰이 '자기만의 질문'을 따로 던지기 때문이다.

SIMULATOR

어텐션 행렬 직접 편집기

마스크
선택 행
점수 패턴 (S 전체를 바꾼다)
선택 행 엔트로피—
최대 가중치—
유효 토큰 수 1/Σp²—
행 합 Σp—
해볼 것: ① 기본 문장에서 'it' 행(선택됨)의 'animal' 칸을 위로 끌어 5 이상으로 올려 보자. A의 그 칸이 짙어지고 아크가 굵어진다. 이제 'street' 칸을 같은 높이로 올리면 가중치가 반반으로 갈린다 — 모델이 "it = animal인지 street인지" 망설이는 상태다. ② 온도를 0.1로 내리면 행마다 최댓값 하나만 남고(엔트로피 → 0, 유효 토큰 수 → 1), 5로 올리면 모든 칸이 비슷해진다. ③ '인과 마스크'를 켜면 대각선 위가 회색(−∞)이 되고, 첫 행은 자기 자신만 볼 수 있어 가중치가 1.00이 된다. '배를 먹었더니…' 문장에서 마스크를 켜면 첫 '배를'은 뒤의 '먹었더니'를 볼 수 없다 — 왼쪽에서 오른쪽으로 생성하는 언어 모델의 한계다. ④ '첫 토큰(sink)' 패턴을 누르고 몇 행만 다른 칸을 올려 보자. 실제 LLM의 많은 헤드가 이런 모양이다(8절). 셀 위에서 마우스 휠로도 ±0.25씩 조정된다.

편집기의 숫자는 교육용으로 손으로 넣은 값이지만, 실제로 학습된 Transformer를 시각화하면 비슷한 장면이 보인다. BERT의 일부 헤드는 대명사에서 선행 명사로 강한 가중치를 보내고(Clark 등, 2019), GPT 계열의 많은 헤드는 첫 토큰이나 문장부호에 가중치 대부분을 몰아 둔다. 단, 어텐션 가중치가 크다고 해서 그 토큰이 출력에 '원인'이 된다고 단정할 수는 없다는 점도 기억해 두자. 값 벡터의 크기와 그 뒤의 \(W_O\)가 실제 기여를 함께 정한다.

유효 토큰 수 \(1/\sum p_j^2\)

가중치가 k개 토큰에 균등하게 퍼져 있으면 정확히 k가 된다. 하나에 몰리면 1, n개 모두에 고르게 퍼지면 n이다. 엔트로피 \(H\)(비트)로 쓰면 \(2^H\)도 비슷한 '몇 개를 보고 있나'의 척도다. 어텐션 헤드가 얼마나 '날카로운지' 비교할 때 자주 쓴다.

Q, K, V는 어디서 오나: 같은 X, 다른 W

앞 절에서는 S를 손으로 넣었다. 실제 모델에서 S를 정하는 것은 학습되는 행렬 \(W_Q\)와 \(W_K\)다. 핵심은 \(S_{ij} = (x_i W_Q)\cdot(x_j W_K)/\sqrt{d_k} = x_i\,(W_Q W_K^\top)\,x_j^\top/\sqrt{d_k}\)라는 점이다. 즉 어텐션 점수는 임베딩 공간에 정의된 학습 가능한 쌍선형 유사도다. 그냥 \(x_i\cdot x_j\)를 쓰면 "비슷한 단어끼리" 서로 보게 될 뿐이지만, \(W_Q W_K^\top\)를 거치면 "대명사는 명사를 찾는다", "각 토큰은 바로 앞 토큰을 찾는다"처럼 비대칭적이고 목적이 있는 관계를 표현할 수 있다. 같은 문장에 서로 다른 W를 가진 헤드가 여러 개 있는 이유도 여기에 있다.

값 쪽도 같다. \(V = XW_V\)는 "선택되면 무엇을 넘겨줄지"를 정한다. 키는 '광고'이고 값은 '내용물'이다. 둘을 분리해 두었기 때문에, 어떤 토큰이 "나는 명사다"라는 키로 주목을 받으면서 정작 넘겨주는 값은 "나는 살아 있는 존재이고 피곤할 수 있다" 같은 다른 성질일 수 있다.

SIMULATOR

Q·K 공간에서 조작하기: W를 바꾸면 패턴이 바뀐다

W_Q (2×2)
W_K (2×2)
숫자 칸을 위아래로 끌면 값이 바뀐다(휠도 가능). W_V = I(항등)로 고정해 V = X.
W 프리셋
옵션
선택 토큰—
가장 많이 보는 토큰—
it → animal 가중치—
출력 o 와 x_animal 거리—
해볼 것: ① '명사→대명사 헤드'를 누르면 'it' 행이 거의 전부 'animal'로 간다. 왼쪽 X 평면에서 'it'의 출력 o(◆)가 xanimal 바로 옆으로 끌려간 것을 확인하자 — 문맥이 반영된 'it'이다. ② 임베딩은 그대로 두고 '이전 토큰 헤드'를 누르면, 같은 X에서 A가 한 칸 아래 대각선(i → i−1)으로 완전히 바뀐다. ③ 가운데 q·k 평면에서 qit(●)를 직접 끌어 ktired(■) 쪽으로 옮겨 보자. 'it' 행만 바뀐다. 키 ■를 끌면 그 토큰의 열 전체가 바뀐다. ④ 왼쪽 평면의 임베딩 점 x를 끌면 그 토큰의 q와 k가 W를 통해 함께 움직인다. ⑤ W 칸을 끌어 W_Q의 크기를 키우면(전체 배율↑) 패턴은 같고 더 날카로워진다 — 온도를 낮춘 것과 같다.

이 시뮬레이터의 2차원 임베딩은 교육용으로 직접 설계한 것이다. 7개 토큰을 원 둘레에 문장 순서대로 배치해 두었는데, 이는 실제 모델에서 토큰 벡터에 위치 정보가 섞여 들어가는 것(6장의 위치 인코딩·RoPE)을 아주 거칠게 흉내 낸 것이다. 실제 모델은 \(d = 768\)(GPT-2 small)~\(4096\)(Llama 3 8B) 차원이고 헤드당 \(d_k = 64\)~\(128\)이라, 의미 축과 위치 축이 서로 다른 방향을 차지하며 공존한다. 2차원에서는 둘을 동시에 담기 어렵기 때문에 '이전 토큰 헤드'는 원 위의 회전으로, '명사→대명사 헤드'는 특정 방향으로의 사영(rank-1 W)으로 구현했다.

\(W_Q W_K^\top\)는 한 덩어리다

점수는 \(W_Q W_K^\top\)라는 곱에만 의존하므로, \(W_Q \to W_Q R\), \(W_K \to W_K R^{-\top}\)로 바꿔도(R은 가역 행렬) 어텐션 패턴은 똑같다. 그래서 해석 가능성 연구에서는 \(W_Q\)와 \(W_K\)를 따로 보지 않고 \(W_{QK} = W_Q W_K^\top\)(d×d, 랭크 ≤ d_k)라는 'QK 회로'로 묶어 분석한다. 같은 이유로 \(W_V W_O\)도 'OV 회로'로 묶는다(Elhage 등, 2021).

마스크: 인과 마스크와 패딩 마스크

어텐션은 기본적으로 모든 토큰이 모든 토큰을 본다. 하지만 상황에 따라 보면 안 되는 위치가 있다. 마스크 \(M\)은 그런 위치의 점수에 \(-\infty\)를 더해, 소프트맥스 뒤 가중치가 정확히 0이 되게 한다(\(e^{-\infty} = 0\)). 구현에서는 \(-\infty\) 대신 \(-10^4\)이나 dtype의 최솟값을 쓰기도 한다.

(a) 인과 마스크 — 하삼각 고양이가고양이가쥐를쥐를쫓았다쫓았다그것은그것은빨랐다빨랐다−∞−∞−∞−∞−∞−∞−∞−∞−∞−∞키 위치 j → (행 = 쿼리 i) (b) 패딩 마스크 — 배치의 두 번째 문장 고양이가고양이가잤다잤다[PAD][PAD][PAD][PAD]−∞−∞−∞−∞−∞−∞−∞−∞[PAD] 열 차단, [PAD] 행은 버림
그림 5-4. (a) 인과 마스크: 행 = 쿼리 위치, 열 = 키 위치. 회색(−∞) 칸은 소프트맥스 후 가중치 0. 첫 토큰은 자기 자신만, 마지막 토큰은 모두를 본다. (b) 패딩 마스크: "고양이가 잤다 [PAD] [PAD]"에서 [PAD] 열을 막는다. 실제로는 두 마스크를 더해(합집합) 함께 쓴다.

위의 편집기(4절)에서 인과 마스크를 켜고 끄며 차이를 다시 확인해 보자. 마스크를 켜면 행마다 소프트맥스의 분모에 들어가는 항이 줄어들기 때문에, 남은 칸의 가중치가 커진다. 첫 행은 항이 하나뿐이라 무조건 1이다. 이 '첫 토큰은 항상 보인다'는 성질은 8절의 어텐션 싱크와 연결된다.

마스크는 소프트맥스 전에

소프트맥스를 먼저 하고 가중치를 0으로 지우면 행 합이 1보다 작아진다. 다시 정규화하면 수학적으로는 같지만, 미래 토큰의 점수가 분모의 최댓값 계산에 섞여 수치가 불안정해질 수 있다. 표준 구현은 항상 점수 단계에서 \(-\infty\)를 더한다.

멀티헤드 어텐션: 여러 관점을 병렬로

한 헤드는 행마다 하나의 확률분포만 만든다. 그런데 한 토큰은 동시에 여러 종류의 정보가 필요하다. 'it'은 지시 대상(animal)도, 바로 앞 단어(because)도, 문장 주어도 알아야 한다. 하나의 소프트맥스로 이 모든 것을 섞으면 서로 경쟁해 흐려진다. 그래서 Transformer는 어텐션을 h개의 헤드로 나눠 병렬로 계산한다.

$$ \mathrm{head}_i = \mathrm{Attention}(XW_Q^{(i)}, XW_K^{(i)}, XW_V^{(i)}), \qquad \mathrm{MHA}(X) = \mathrm{Concat}(\mathrm{head}_1,\dots,\mathrm{head}_h)\,W_O $$
각 헤드의 차원은 \(d_{head} = d_{model}/h\). GPT-2 small은 768 = 12 × 64, Llama 3 8B는 4096 = 32 × 128. 헤드 수를 늘려도 전체 파라미터·연산량은 거의 같다(차원을 나눠 쓰므로). \(W_O\in\mathbb{R}^{d\times d}\)가 헤드 출력들을 다시 섞는다.
Xn×768 head₁: Q,K,V n×64 → 이전 토큰 head₂: 구문 관계 ⋯ head₁₂: 첫 토큰(sink) Concatn×(12·64) · W_O 출력n×768
그림 5-5. GPT-2 small 기준 멀티헤드 어텐션. 768차원을 64차원 헤드 12개로 나눠 각자 다른 패턴으로 정보를 모은 뒤, 이어 붙이고(Concat) \(W_O\)로 섞는다. 구현에서는 12개 헤드를 텐서의 한 축으로 두고 한 번의 배치 행렬곱으로 계산한다.
SIMULATOR

멀티헤드: 네 가지 관점과 그 혼합

헤드 켜기/끄기 (혼합에 포함)
선택 헤드 / 토큰—
선택 헤드 최대 가중치—
혼합 결과 유효 토큰 수—
해볼 것: ① 위의 작은 히트맵 4개 중 하나를 누르면 그 헤드가 선택되고, 오른쪽 아크가 그 헤드의 가중치를 보여 준다. 혼합 행렬의 행이나 아크의 토큰을 누르면 쿼리 토큰이 바뀐다. ② 기본 선택인 'ran'(9번)에서 헤드를 하나씩 바꿔 보자. H1은 바로 앞 'quickly', H2는 주어 'dog'(7번), H3은 같은 단어가 앞에 없어 자기 자신, H4는 문장 첫 'the'로 간다 — 한 토큰이 네 곳에서 동시에 정보를 받는다. ③ 두 번째 'dog'(7번)를 고르면 H3이 첫 'dog'(4번)를 찾는다. ④ 헤드를 끄면 혼합 결과에서 그 패턴이 사라진다. β를 낮추면 모든 헤드가 흐려진다.

시뮬레이터의 네 헤드는 실제 모델에서 자주 관찰되는 패턴을 규칙으로 손설계한 것이다(교육용). 아래 '혼합'은 모든 헤드가 같은 V를 쓰고 \(W_O\)가 헤드 출력을 켜진 헤드 수로 나눠 더하는 특별한 경우의 유효 어텐션 \(\tfrac{1}{h'}\sum_h A_h\)다. 이 경우 \(\sum_h g_h A_h V = (\sum_h g_h A_h)V\)이므로 정확히 성립한다. 일반적인 \(W_O\)에서는 헤드마다 다른 값 부분공간을 쓰므로, 행렬 하나로 요약되지 않는다. 그 '요약되지 않음'이 바로 멀티헤드의 표현력이다.

헤드는 정말 다 필요할까?

Michel 등(2019)은 학습된 Transformer에서 상당수 헤드를 지워도 성능이 거의 떨어지지 않음을 보였다. 반면 일부 헤드(예: 인덕션 헤드)는 지우면 특정 능력이 무너진다. 헤드는 역할이 균일하지 않고, 소수의 핵심 헤드와 많은 '보조·중복' 헤드로 이루어진다. 이 관찰은 9·10절의 효율화(MQA, GQA, 헤드 가지치기)로 이어진다.

어텐션 패턴 동물원

학습된 언어 모델의 어텐션 행렬을 열어 보면, 몇 가지 '종'이 반복해서 나타난다. 해석 가능성(mechanistic interpretability) 연구는 이런 패턴이 무슨 계산을 하는지 역설계한다. 아래 그림은 대표 패턴을 교육용으로 단순화해 그린 것이다(실제 헤드는 훨씬 지저분하고 여러 패턴이 섞여 있다).

어텐션 싱크 이전 토큰 인덕션 헤드 슬라이딩 윈도우 첫 토큰 열이 짙다 대각선 한 칸 아래 "ABCD ABCD": 반복 구간에서 '이전 등장 +1' 위치 최근 w개만 (w = 3)
그림 5-6. 인과 마스크가 있는 8토큰 예시. 색이 짙을수록 큰 가중치, 회색은 마스크. 인덕션 헤드 그림의 입력은 "A B C D A B C D"로, 두 번째 A(4번)는 첫 A 다음에 왔던 B(1번)를 본다.

인덕션 헤드는 어떻게 작동하나

두 층의 협업이다. 1층의 이전 토큰 헤드가 각 위치 j에 "내 바로 앞 토큰은 \(t_{j-1}\)이다"라는 정보를 써넣는다. 2층의 인덕션 헤드는 현재 위치 i의 토큰 \(t_i\)를 쿼리로, 각 위치에 적힌 '앞 토큰' 정보를 키로 쓴다. 그러면 \(t_{j-1} = t_i\)인 위치 j, 즉 "예전에 지금과 같은 토큰 바로 다음에 왔던 자리"의 점수가 높아진다. 그 위치의 토큰 \(t_j\)를 값으로 복사하면 다음 토큰 예측이 된다.

$$ S^{(2)}_{ij} = \beta\,\big[\,\mathrm{onehot}(t_i)\cdot \underbrace{\textstyle\sum_l A^{(1)}_{jl}\,\mathrm{onehot}(t_l)}_{\text{1층이 쓴 '앞 토큰'}}\big], \qquad p(\text{다음}) = \sum_j A^{(2)}_{ij}\,\mathrm{onehot}(t_j) $$
SIMULATOR

인덕션 헤드 시연: 2층 회로를 실제로 계산하기

시퀀스
선택 위치의 예측—
앞 절반 정확도—
뒤 절반 정확도—
해볼 것: ① 토큰 줄에서 뒤 절반(반복 구간)의 아무 토큰이나 누르자. 위쪽 보라 아크(2층)가 앞 절반에서 '같은 토큰 다음 자리'를 가리키고, 그 아래 분홍 화살표(1층)가 "그 자리의 앞 토큰이 지금과 같다"는 근거를 보여 준다. 아래 막대가 예측 분포다. ② 앞 절반의 토큰은 아직 반복이 없어 예측이 거의 균등에 가깝다(정확도 ≈ 우연). 뒤 절반은 거의 다 맞힌다. '선택 토큰 바꾸기'로 앞 절반에 같은 토큰을 두 번 만들면 인덕션 헤드가 두 후보 사이에서 갈라진다. ③ '반복' 체크를 끄면 뒤 절반 정확도도 우연 수준으로 떨어진다. ④ 오른쪽 A(2) 히트맵에서 반복 구간에 생기는 '오프셋 대각선'이 인덕션 헤드의 지문이다. β = 0이면 모든 칸이 균등해져 기억이 사라진다.

이 시뮬레이터의 두 층은 학습된 가중치가 아니라 원-핫 벡터와 위치 규칙으로 손설계한 것이다. 그러나 계산 구조(1층 어텐션의 출력이 2층의 키가 되는 'K-합성')는 Elhage 등(2021)과 Olsson 등(2022)이 실제 모델에서 찾아낸 회로와 같다. 해석 가능성 연구는 이처럼 어텐션 헤드, MLP 뉴런, 그리고 최근에는 희소 오토인코더로 찾은 '특징'들을 회로로 엮어 모델 내부를 설명하려 한다. 아직은 작은 모델과 특정 행동에 대한 부분적 설명이 대부분이라는 점을 함께 기억하자.

계산량과 메모리: O(n²)와 FlashAttention

어텐션의 비용은 두 부분으로 나뉜다. Q·K·V·O 투영은 토큰마다 독립이라 길이에 선형이다(\(4\cdot 2nd^2\) FLOPs). 반면 점수 \(QK^\top\)와 \(AV\)는 모든 쌍을 다루므로 길이의 제곱이다(\(2\cdot 2n^2 d\) FLOPs). 둘이 같아지는 지점은 \(n = 2d\)로, Llama 3 8B(d = 4096)라면 약 8K 토큰부터 어텐션 쌍 계산이 투영보다 비싸진다. 메모리는 더 아프다. 점수 행렬 S를 그대로 만들면 헤드마다 \(n^2\)개 원소가 필요하다.

SIMULATOR

시퀀스 길이 vs 비용 (한 층, 배치 1, BF16)

d_model
S 행렬 메모리 (h·n²·2B)—
FlashAttention 추가 메모리—
어텐션 FLOPs (4n²d)—
투영 FLOPs (8nd²)—
H100 이상적 시간 (989 TFLOPS)—
해볼 것: ① d = 4096, h = 32에서 n을 32K(215)로 올리면 S 행렬 하나가 한 층에 64 GiB다. H100 한 장(80 GB)이 층 하나의 점수 행렬로 거의 찬다. ② n을 두 배로 할 때 S 메모리와 어텐션 FLOPs는 4배, 투영 FLOPs는 2배가 되는 것을 로그 축의 기울기(2 대 1)로 확인하자. ③ 헤드 수를 바꾸면 S 메모리는 h에 비례하지만 FLOPs는 그대로다(헤드 차원이 d/h로 줄어들기 때문). ④ FlashAttention 곡선은 n에 선형이다.

FlashAttention: 큰 행렬을 만들지 않는다

GPU에서 병목은 연산이 아니라 메모리 이동인 경우가 많다. H100의 HBM3는 약 3.35 TB/s지만, 연산은 BF16 dense 약 989 TFLOPS라 바이트당 수백 번 연산할 수 있다. 순진한 구현은 S(n×n)를 HBM에 쓰고, 다시 읽어 소프트맥스하고, A를 쓰고, 다시 읽어 V와 곱한다. Dao 등(2022)의 FlashAttention은 Q, K, V를 작은 타일로 잘라 칩 위의 빠른 SRAM(공유 메모리)에 올리고, 한 타일 안에서 점수·소프트맥스·V 곱을 모두 끝낸 뒤 출력만 HBM에 쓴다. n×n 행렬은 어디에도 통째로 존재하지 않는다. 연산량은 같지만(오히려 역전파 때 재계산으로 약간 늘지만) 메모리 읽기·쓰기가 크게 줄어 수 배 빨라지고, 추가 메모리는 O(n)이다.

타일 단위로 K·V 블록을 훑으며 출력 블록을 누적 Q Qᵢ Kᵀ Kⱼ S는 타일(Bq×Bk)만 잠시 SRAM에 V Vⱼ SRAM (SM당 ~228 KB, 매우 빠름) Sᵢⱼ = QᵢKⱼᵀ, m ← max, ℓ ← 재조정 합 Oᵢ ← Oᵢ·e^(m_old−m_new) + e^(Sᵢⱼ−m)Vⱼ HBM (80 GB, ~3.35 TB/s) Q, K, V, O만 저장 — n×n 행렬 없음 추가 저장: 행마다 m, ℓ (O(n))
그림 5-7. FlashAttention의 타일링. Q의 블록 하나(Qᵢ)를 SRAM에 올려 두고 K, V 블록을 차례로 가져와 점수 타일을 계산한다. 소프트맥스의 분모를 아직 모르는 상태에서 블록을 하나씩 처리하려면 아래의 '온라인 소프트맥스'가 필요하다. SRAM 용량은 H100 기준 SM당 약 228 KB(2023).

문제는 소프트맥스다. \(a_j = e^{s_j}/\sum_l e^{s_l}\)는 분모에 행 전체의 합이 들어가고, 수치 안정성을 위해 보통 행 최댓값 \(m\)을 빼서 \(e^{s_j - m}\)으로 계산한다. 블록을 하나씩 보면 최댓값도 합도 아직 모른다. 온라인 소프트맥스(online softmax, Milakov & Gimelshein, 2018)는 지금까지의 최댓값 \(m\)과 합 \(\ell\)을 들고 다니다가, 새 블록에서 최댓값이 커지면 기존 합과 누적 출력에 \(e^{m_{old}-m_{new}}\)를 곱해 소급 보정한다.

$$ m' = \max(m, \max_{j\in B} s_j),\quad \ell' = \ell\, e^{m-m'} + \sum_{j\in B} e^{s_j - m'},\quad \mathbf{o}' = \mathbf{o}\, e^{m-m'} + \sum_{j\in B} e^{s_j-m'}\,v_j $$
모든 블록을 처리한 뒤 \(\mathbf{o}/\ell\)이 정확히 \(\sum_j \mathrm{softmax}(s)_j\, v_j\)와 같다(근사가 아니다). 처음에는 \(m = -\infty,\ \ell = 0,\ \mathbf{o} = 0\).
SIMULATOR

온라인 소프트맥스: 블록 단위로 최댓값·합 갱신

막대를 위아래로 드래그
블록 크기 B
현재 m / ℓ—
온라인 o/ℓ—
일반 softmax·v—
차이 |Δ|—
해볼 것: ① '다음 블록'을 눌러 한 블록씩 처리하자. 위쪽 점선이 지금까지의 최댓값 m이고, 아래 막대는 '지금까지 본 것만으로 정규화한' 가중치다. 새 블록에서 더 큰 점수가 나오면 앞 블록들의 가중치가 일제히 작아지는(재조정 계수 em_old−m_new < 1) 것을 확인하자. ② 끝까지 처리하면 온라인 결과와 일반 소프트맥스 결과의 차이가 10−16 수준(부동소수점 반올림)이다. ③ 마지막 블록의 막대 하나를 가장 높게 끌어 올린 뒤 처음부터 다시 진행해 보자. 마지막 단계에서 큰 보정이 일어난다. ④ 블록 크기를 바꿔도 최종 결과는 같다.

FlashAttention은 2022년 이후 사실상 표준이 되었고, FlashAttention-2(2023)는 병렬화를 개선했으며, FlashAttention-3(2024)는 H100의 비동기 텐서 코어와 FP8을 활용한다. PyTorch의 scaled_dot_product_attention도 조건이 맞으면 이런 커널을 자동으로 쓴다. 중요한 점은 이것이 정확한 어텐션이라는 것이다. 근사로 비용을 줄이는 선형 어텐션·희소 어텐션과 달리, 결과는 순진한 구현과 같고 메모리 접근 순서만 바꿨다.

셀프·크로스 어텐션, MHA·MQA·GQA, 슬라이딩 윈도우

셀프 어텐션 vs 크로스 어텐션

지금까지는 Q, K, V가 모두 같은 X에서 나왔다. 이것이 셀프 어텐션이다. 크로스 어텐션(cross-attention)에서는 쿼리는 한 시퀀스에서, 키와 값은 다른 시퀀스에서 온다. 원래 Transformer의 번역 디코더는 번역문 토큰의 쿼리로 원문 인코더 출력의 키·값을 조회했다(그림 5-1의 b가 바로 그것). 멀티모달 모델에서는 텍스트 쿼리가 이미지 패치 임베딩을 키·값으로 조회하고, Stable Diffusion 같은 이미지 생성 모델은 이미지 특징이 쿼리, 프롬프트 텍스트 임베딩이 키·값이다. 크로스 어텐션의 점수 행렬은 \(n_q\times n_{kv}\)로 정사각형이 아니며, 인과 마스크도 보통 필요 없다.

셀프 어텐션: Q, K, V ← 같은 X X (n×d) Q K V → A: n×n 언어 모델의 모든 층, BERT, ViT 크로스 어텐션: Q ← Y, (K, V) ← Z Y: 번역문 / 텍스트 Z: 원문 / 이미지 Q K V → A: n_q×n_kv 번역 디코더, 멀티모달, 확산 모델의 텍스트 조건
그림 5-8. 셀프 어텐션은 한 시퀀스 안에서, 크로스 어텐션은 두 시퀀스 사이에서 정보를 옮긴다. 수식은 똑같고 Q와 K·V의 출처만 다르다.

MHA, MQA, GQA: KV 헤드를 공유하기

생성 단계에서는 이전 토큰들의 K, V를 저장해 두는 KV 캐시(8장)가 메모리의 주인공이 된다. 표준 멀티헤드(MHA)는 쿼리 헤드마다 자기 K, V 헤드가 있다. MQA(Multi-Query Attention, Shazeer 2019)는 모든 쿼리 헤드가 K, V 헤드 하나를 공유해 캐시를 h배 줄이지만 품질이 약간 떨어질 수 있다. GQA(Grouped-Query Attention, Ainslie 등 2023)는 그 중간으로, 쿼리 헤드를 g개 그룹으로 묶어 그룹마다 K, V 헤드 하나를 둔다. Llama 3 8B는 쿼리 헤드 32개, KV 헤드 8개(그룹당 쿼리 4개)를 쓴다.

MHA · KV 헤드 8개qqqqqqqqkvkvkvkvkvkvkvkvGQA · KV 헤드 2개qqqqqqqqkvkvMQA · KV 헤드 1개qqqqqqqqkv
그림 5-9. 쿼리 헤드 8개 기준. MHA는 KV 헤드 8개, GQA(그룹 4)는 2개, MQA는 1개. 쿼리 헤드 수(어텐션 패턴의 다양성)는 그대로 두고, 캐시에 저장할 K·V만 줄인다.
Llama 3 8B 토큰당 KV 캐시 (GQA, 8 KV헤드)128 KiB2 × 32층 × 8 × 128 × 2 B
같은 모델을 MHA(32 KV헤드)로 만들면512 KiB4배
8K 토큰 문맥 하나 (GQA)1 GiBMHA면 4 GiB
MQA(1 KV헤드)라면16 KiB캐시 1/32, 품질 손실 위험

슬라이딩 윈도우와 그 밖의 변형

슬라이딩 윈도우 어텐션은 각 토큰이 최근 w개 토큰만 보게 해 비용을 \(O(nw)\)로 줄인다(그림 5-6의 넷째 패턴). Mistral 7B(2023)는 w = 4096을 썼고, 층을 쌓으면 정보가 층마다 w씩 더 멀리 전파되므로 L층이면 이론적으로 약 L·w 거리까지 닿는다. Gemma 2·3처럼 지역(윈도우) 층과 전역 층을 번갈아 쓰는 모델도 많다. 이 밖에 키·값을 저차원 잠재 벡터로 압축해 캐시하는 MLA(DeepSeek-V2, 2024), 소프트맥스를 커널 근사로 바꾼 선형 어텐션 계열 등이 있다. 위치 정보를 Q, K의 회전으로 넣는 RoPE는 6장에서, KV 캐시와 디코딩 속도는 8장에서 자세히 다룬다.

방식점수 계산 범위시간KV 캐시 / 토큰·층예
MHA전체 (인과)O(n²d)2·h·d_headGPT-2, GPT-3, Llama 2 7B
MQA전체O(n²d)2·1·d_headPaLM, Falcon
GQA전체O(n²d)2·g·d_headLlama 3 (g = 8), Mistral 7B
슬라이딩 윈도우최근 w개O(nwd)최대 w개 토큰만Mistral 7B, Gemma 2·3 지역 층
FlashAttention(구현 기법) 전체, 정확O(n²d)변화 없음거의 모든 현대 학습·추론

핵심 정리

  1. 어텐션은 부드러운 사전 검색이다. 쿼리와 키의 내적 → 소프트맥스 가중치 → 값의 가중 평균. 온도가 0에 가까우면 hard lookup, 크면 단순 평균이다.
  2. \(\mathrm{Attention}(Q,K,V) = \mathrm{softmax}(QK^\top/\sqrt{d_k} + M)V\). S와 A는 n×n이고, 소프트맥스는 행별로 적용되며 행 안의 상대 차이에만 반응한다.
  3. 내적의 분산은 \(d_k\)에 비례하므로 \(\sqrt{d_k}\)로 나눠 분산을 1로 맞춘다. 나누지 않으면 소프트맥스가 포화되어 기울기가 사라진다.
  4. \(Q = XW_Q,\ K = XW_K,\ V = XW_V\). 점수는 \(x_i W_Q W_K^\top x_j^\top\)라는 학습된 쌍선형 유사도라, 같은 임베딩에서도 W에 따라 '명사→대명사', '이전 토큰' 같은 전혀 다른 패턴이 나온다.
  5. 인과 마스크(j > i를 −∞)는 디코더 언어 모델의 병렬 학습과 KV 캐시를 가능하게 한다. 패딩 마스크는 [PAD] 열을 막는다. 마스크는 소프트맥스 전에 더한다.
  6. 멀티헤드는 d_model을 h개 헤드(d_head = d/h)로 나눠 여러 관계를 병렬로 본 뒤 Concat·W_O로 섞는다. 실제 헤드에는 어텐션 싱크, 이전 토큰 헤드, 인덕션 헤드 같은 반복 패턴이 있다.
  7. 어텐션 쌍 계산은 O(n²d), 점수 행렬 메모리는 O(h·n²)이다. FlashAttention은 타일링과 온라인 소프트맥스로 n×n 행렬을 만들지 않고 정확히 같은 결과를 O(n) 추가 메모리로 낸다.
  8. 크로스 어텐션은 Q와 K·V의 출처가 다르다. MQA·GQA는 KV 헤드를 공유해 캐시를 줄인다(Llama 3 8B: 32 Q헤드, 8 KV헤드 → 토큰당 128 KiB). 슬라이딩 윈도우는 범위를 최근 w개로 제한한다.

확인 퀴즈

1. 점수 행렬 S의 한 행 전체에 같은 상수 5를 더하면 그 행의 어텐션 가중치 A는?

소프트맥스는 \(e^{s_j+c}/\sum_l e^{s_l+c} = e^{s_j}/\sum_l e^{s_l}\)이라 행 안의 상수 이동에 불변이다. 그래서 수치 안정화를 위해 최댓값을 빼도 결과가 같고, 온라인 소프트맥스도 이 성질을 이용한다. 반면 상수를 곱하면(온도 변화) 분포의 날카로움이 바뀐다.

2. q, k의 성분이 독립이고 평균 0, 분산 1일 때 d_k = 256이면 q·k의 표준편차는 대략?

분산이 \(d_k = 256\)이므로 표준편차는 \(\sqrt{256} = 16\). 점수가 ±16 수준으로 퍼지면 소프트맥스가 거의 원-핫으로 포화된다. \(\sqrt{d_k} = 16\)으로 나누면 표준편차가 1로 돌아온다.

3. 인과 마스크가 적용된 길이 6 시퀀스에서, 첫 번째 토큰(위치 0)의 어텐션 가중치 행은 항상 어떻게 되는가?

위치 0은 j > 0을 모두 볼 수 없으므로 소프트맥스에 남는 항이 자기 자신 하나뿐이고, 가중치는 무조건 1이다. 모든 위치에서 항상 보이는 첫 토큰은 '볼 게 없을 때 가중치를 버리는 곳', 즉 어텐션 싱크가 되기 쉽다.

4. d_model = 4096, 헤드 32개인 층에서 헤드 수를 64개로 늘리고 d_head를 64로 줄였다. 변하는 것은?

h·d_head = 4096이 유지되므로 투영 파라미터와 쌍 계산 FLOPs(4n²d)는 그대로다. 그러나 점수 행렬은 헤드마다 n×n 하나씩이라 h에 비례해 늘어난다. FlashAttention은 이 행렬을 만들지 않으므로 이 문제를 피한다.

5. 시퀀스 "x y z … x"의 마지막 위치에서 인덕션 헤드가 높은 가중치를 주는 위치는?

1층 이전 토큰 헤드가 y 위치에 "내 앞은 x"라는 정보를 써 둔다. 2층 인덕션 헤드는 현재 토큰 x를 쿼리로 "앞이 x인 위치"를 찾으므로 y를 보고, 그 값(y)을 복사해 다음 토큰으로 y를 예측한다. 첫 번째 x 자체를 보는 것은 '동일 단어' 헤드다.

6. Llama 3 8B(32층, KV 헤드 8개, d_head = 128, BF16)의 KV 캐시는 토큰당 얼마이고, GQA가 줄이는 것은 무엇인가?

2(K, V) × 32층 × 8헤드 × 128 × 2 B = 131,072 B = 128 KiB. MHA(32 KV 헤드)였다면 512 KiB다. GQA는 쿼리 헤드 4개가 K·V 헤드 하나를 공유하게 해 캐시와 메모리 대역폭을 1/4로 줄이고, 쿼리 쪽 패턴 다양성은 유지한다.