표지 0/163

LLM 이론 트랙 · 통근 학습용 · 합본

확률분포에서 프로덕션까지

한 세션이 통근 한 번이다. 163개 세션 전문이 이 한 페이지에 들어 있고, 왼쪽 위 목차 버튼으로 모듈과 세션을 오갈 수 있다. 읽음 표시와 마지막 위치는 브라우저에 저장되므로 닫았다 다시 열어도 이어서 읽으면 된다.

163Sessions
13Modules
28Interactive
20–30mPer Session

M0세션 1–55개

출발점

LLM이 무엇을 하는 물건인지에 대한 감각을 먼저 세운다. 이후 모든 모듈은 여기서 세운 한 문장 위에 얹힌다.

Session 1LLM은 정확히 무엇을 학습하는가

거대 언어 모델이 학습하는 것은 단 하나다. 지금까지의 토큰이 주어졌을 때 다음 토큰이 무엇일지에 대한 확률분포. 문법도, 지식도, 추론도 별도로 배우지 않는다. 전부 이 하나의 목표를 좇는 과정에서 생겨난 부산물이다.

이 문장은 과장이 아니라 문자 그대로의 기술적 사실이다. 그리고 이 사실을 정확히 붙들고 있는지 아닌지가 앞으로 나올 모든 내용의 이해도를 가른다. 예를 들어 "왜 모델이 없는 사실을 지어내는가", "왜 파인튜닝으로 지식을 주입하기 어려운가", "왜 같은 질문에 매번 다른 답이 나오는가" 같은 질문들은 전부 이 한 문장에서 곧바로 따라 나온다. 반대로 이 문장이 흐릿하면 그 질문들은 각각 따로 외워야 하는 별개의 사실이 되어 버린다.

학습이 실제로 벌어지는 방식

학습 데이터는 그냥 긴 텍스트다. 웹 문서, 책, 코드, 위키 같은 것들을 이어 붙인 거대한 문자열이라고 생각하면 된다. 여기에는 "질문과 정답" 같은 구조가 없다. 사람이 라벨을 붙여 준 것도 아니다. 정답은 텍스트 자신이 이미 갖고 있다. 어떤 위치에서든, 그 다음에 실제로 등장한 토큰이 곧 정답이기 때문이다.

그래서 학습 절차는 이렇게 된다. 텍스트를 토큰이라는 단위로 쪼갠다. 토큰이 정확히 무엇인지는 M3에서 자세히 다루겠지만, 지금은 "단어보다 잘고 글자보다 굵은 조각"이라고만 알아 두면 충분하다. 그다음 이 토큰 열을 모델에 넣고, 각 위치마다 "여기 다음에 올 토큰은 무엇인가"를 예측하게 한다. 모델의 예측은 특정 토큰 하나가 아니라 어휘 전체에 대한 확률분포다. 어휘 크기가 10만이라면, 모델은 위치마다 10만 개의 숫자를 내놓고 그 합이 1이 된다.

이제 실제 정답과 비교한다. 실제로 등장한 토큰에 모델이 배정한 확률이 낮았다면 손실이 크고, 높았다면 손실이 작다. 이 손실을 줄이는 방향으로 파라미터를 조금씩 옮긴다. 이것을 수십조 개 토큰에 대해 반복한 결과가 우리가 아는 LLM이다.

한 문장에서 학습 신호가 만들어지는 방식

나는어제학교 갔다 어제학교 갔다 입력 (지금까지의 토큰) 정답 (한 칸 민 것)
정답 라벨은 입력을 한 칸 민 것이다. 따로 만들 필요가 없다. 그래서 인터넷에 있는 아무 텍스트나 그대로 학습 데이터가 되고, 이 값싼 정답 공급이 오늘날의 규모를 가능하게 했다.

한 가지 짚어 둘 것이 있다. 위 그림에서 네 개의 예측은 순서대로 하나씩 이루어지는 것이 아니다. 학습할 때는 전체 문장을 한 번에 넣고 모든 위치의 예측을 동시에 계산한다. 각 위치가 자기보다 뒤에 있는 토큰을 훔쳐보지 못하도록 막는 장치만 있으면 되는데, 그 장치가 M4에서 나올 causal mask다. 반면 실제로 문장을 생성할 때는 어쩔 수 없이 한 토큰씩 순차적으로 만들어야 한다. 학습은 병렬이고 생성은 순차라는 이 비대칭이 나중에 추론 성능 문제의 뿌리가 된다.

이 하나에서 따라 나오는 것들

모델의 출력이 확률분포라는 사실은 실무에서 곧바로 두 가지를 함의한다.

첫째, 출력은 결정되어 있지 않다. 확률분포에서 하나를 골라야 하는데, 그 고르는 방식이 샘플링이고 온도나 top-p 같은 설정이 여기에 붙는다. 같은 입력에 다른 답이 나오는 것은 버그가 아니라 설계다.

둘째, 모델에게 "모른다"는 상태가 기본적으로 없다. 어떤 입력이 들어와도 어휘 전체에 대한 확률분포는 반드시 만들어진다. 확률이 골고루 퍼져 있는 상태, 즉 모델이 확신하지 못하는 상태는 존재하지만, 그것이 자동으로 "모르겠습니다"라는 출력으로 이어지지는 않는다. 환각이라고 부르는 현상의 뿌리가 여기에 있다. 모델은 거짓말을 하도록 학습된 것이 아니라, 애초에 침묵할 수 있는 출구가 없는 구조인 것이다.

백엔드의 언어로

가장 가까운 대응물은 거대한 조건부 확률 테이블을 압축한 것이다. 원리적으로는 "앞 문맥 → 다음 토큰 확률"이라는 조회 테이블을 만들고 싶지만, 가능한 앞 문맥의 수가 천문학적이라 저장이 불가능하다. 그래서 테이블을 저장하는 대신, 그 테이블을 근사하는 함수를 파라미터 수십억 개로 학습한다. LLM은 그 함수다.

다만 비유는 여기서 깨진다. 조회 테이블은 등록되지 않은 키에 대해 아무 값도 주지 못하지만, 학습된 함수는 한 번도 본 적 없는 입력에 대해서도 그럴듯한 값을 내놓는다. 이것이 일반화라고 부르는 능력이자 동시에 환각의 원천이다. 캐시 미스가 없는 캐시를 상상해 보면 된다. 항상 무언가를 돌려주지만, 그 값이 진짜인지는 보장하지 않는다.

흔히 틀리는 지점

확인 질문

  1. LLM의 학습 데이터에 사람이 붙인 라벨이 필요 없는 이유를 한 문장으로 말해 보라.
  2. 모델이 어떤 위치에서 내놓는 출력의 정확한 형태는 무엇인가? 숫자 몇 개이고, 그 숫자들 사이에는 어떤 제약이 있는가?
  3. 모델에게 "모른다"는 출력이 구조적으로 어려운 이유를 설명해 보라.
확인

1. 정답이 텍스트 자체에 이미 들어 있기 때문이다. 어느 위치에서든 실제로 그다음에 등장한 토큰이 곧 정답이므로, 입력을 한 칸 민 것이 그대로 라벨이 된다.

2. 어휘 크기만큼의 숫자다. 어휘가 10만이면 10만 개. 모두 0 이상이어야 하고 전부 더하면 정확히 1이 되어야 한다. 즉 어휘 전체에 대한 확률분포다.

3. 어떤 입력이 들어와도 확률분포는 반드시 만들어지기 때문이다. 확신이 낮은 상태는 분포가 평평한 형태로 존재하지만, 그 상태를 "모르겠다"는 출력으로 바꿔 주는 장치는 구조 안에 없다. 그런 행동은 나중에 사후학습(M6)으로 따로 가르쳐야 한다.

다음으로

다음 세션은 방금 유보한 질문을 정면으로 다룬다. 다음 토큰 맞히기라는 단순한 목표가 어떻게 번역, 요약, 코드 작성 같은 능력으로 이어지는가.


Session 2next-token prediction이 왜 지능처럼 보이는가

다음 토큰을 잘 맞히려면 텍스트를 압축해야 하고, 잘 압축하려면 텍스트가 말하는 내용의 구조를 어느 정도 파악해야 한다. 능력은 목표에서 직접 오는 것이 아니라, 목표를 아주 잘 달성하려는 압박에서 파생된다.

앞 세션의 설명을 듣고 나면 자연스럽게 이런 반응이 나온다. 다음 단어 맞히기는 휴대폰 자판의 자동완성이 하는 일 아닌가. 그것과 코드를 짜고 논문을 요약하는 능력 사이에는 건널 수 없는 간극이 있어 보인다. 이 세션은 그 간극이 왜 생각보다 좁은지를 설명한다.

압축이라는 관점

핵심 착상은 이것이다. 다음 토큰을 잘 예측하는 능력과 텍스트를 잘 압축하는 능력은 같은 것이다.

압축의 원리를 떠올려 보자. 어떤 기호가 나올 확률을 정확히 알고 있다면, 자주 나오는 기호에 짧은 부호를, 드물게 나오는 기호에 긴 부호를 배정해서 전체 길이를 줄일 수 있다. 확률 예측이 정확할수록 압축률이 좋아진다. 이 관계는 비유가 아니라 정보이론의 정리이고, 우리가 M1에서 다룰 cross-entropy가 정확히 "이 예측을 썼을 때 필요한 평균 부호 길이"를 재는 양이다. 언어 모델의 손실 함수가 곧 압축률의 측정치라는 뜻이다.

그러면 질문을 바꿔 볼 수 있다. 인터넷 텍스트를 아주 잘 압축하려면 무엇을 알아야 하는가?

우선 문자 수준의 규칙성을 알면 조금 압축된다. q 다음에는 u가 온다는 식이다. 단어 수준으로 올라가면 더 압축된다. "학교" 다음에 조사가 온다는 것을 알면 후보가 크게 줄어든다. 여기까지는 자동완성의 영역이다.

그런데 압축률을 더 밀어붙이면 사정이 달라진다. 2+3= 다음에 오는 토큰을 맞히려면 덧셈을 해야 한다. 산술 규칙을 모르는 모델은 이 자리에서 확률을 넓게 퍼뜨릴 수밖에 없고, 그만큼 손실을 지불한다. 긴 소설에서 300페이지 앞에 등장한 인물의 이름을 다시 맞히려면 인물 관계를 어떤 형태로든 추적해야 한다. 파이썬 코드에서 함수 본문의 다음 줄을 맞히려면 위에서 정의한 변수의 타입과 스코프를 알아야 한다. 이 모든 것이 손실을 줄이라는 압박에서 나온다.

이 모듈에서 가장 중요한 한 단락

능력이 학습 목표에 명시적으로 들어 있는 것이 아니라, 목표를 극한까지 밀어붙이는 과정에서 수단으로 획득된다는 것. 아무도 모델에게 덧셈을 가르치지 않았다. 덧셈을 할 줄 알면 다음 토큰 예측 손실이 줄어들기 때문에 학습된 것이다. 이것이 이 분야에서 가장 반직관적이면서 가장 많은 것을 설명하는 관찰이다.

이 관점의 한계

압축 이야기는 강력하지만 만능은 아니다. 세 가지 지점에서 조심해야 한다.

첫째, 압축을 잘한다고 해서 그것이 사람이 말하는 의미의 "이해"라고 단정할 수는 없다. 이 논쟁은 아직 결론이 나지 않았고, 실무자로서는 어느 쪽 입장을 취하든 예측되는 시스템 동작이 크게 달라지지 않는다. 다만 "압축을 잘하니까 이해한 것이다"라고 단언하는 것도, "통계일 뿐이니 이해가 아니다"라고 단언하는 것도 둘 다 근거 이상으로 나아간 주장이라는 점은 알아 두는 편이 좋다.

둘째, 학습 데이터에 없는 종류의 규칙성은 획득되지 않는다. 압축의 대상이 인터넷 텍스트라면, 인터넷 텍스트에서 손실을 줄이는 데 도움이 되는 구조만 학습된다. 사내 시스템의 특수한 업무 규칙 같은 것은 아무리 모델을 키워도 나오지 않는다. 이것이 RAG와 파인튜닝이 필요한 이유의 절반이다.

셋째, 사전학습된 모델은 유용한 답을 하도록 학습된 것이 아니다. 텍스트를 이어 쓰도록 학습됐을 뿐이다. 질문을 던지면 답 대신 비슷한 질문을 더 나열하는 것이 오히려 자연스러운 반응이다. 인터넷에는 질문 목록이 흔하니까. 이 간극을 메우는 것이 M6의 사후학습이고, 그래서 베이스 모델과 채팅 모델은 전혀 다른 물건처럼 행동한다.

백엔드의 언어로

목표 함수 하나를 극단적으로 최적화했더니 예상하지 않은 부수 능력이 딸려 나오는 구조는, 지표 하나를 강하게 걸었을 때 시스템이 예상 못 한 방향으로 최적화되는 현상과 성격이 같다. 다만 여기서는 그 부작용이 대체로 유용한 방향이었다는 점이 다르다.

그리고 이 구조에는 대가가 따른다. 목표에 없는 것은 보장되지 않는다는 점이다. 사실 정확성은 학습 목표에 들어 있지 않다. 모델은 "참인 문장"이 아니라 "그럴듯한 다음 토큰"을 최적화했다. 참인 문장이 대체로 그럴듯하기 때문에 상관관계가 높을 뿐, 둘은 다른 목표다. 이 차이를 잊으면 평가 설계(M12)에서 반드시 대가를 치른다.

흔히 틀리는 지점

확인 질문

  1. 다음 토큰 예측과 압축이 같은 문제라는 주장을 두 문장으로 설명해 보라.
  2. 모델이 산술을 학습하게 되는 과정을, "누가 가르쳤는가"라는 질문에 답하는 형태로 말해 보라.
  3. 사전학습만 마친 모델에게 질문을 던지면 왜 답 대신 엉뚱한 것이 나올 수 있는가?
확인

1. 압축률은 각 기호의 확률을 얼마나 정확히 알고 있는가로 결정된다. 다음 토큰의 확률을 정확히 예측할수록 짧은 부호를 배정할 수 있으므로, 예측 정확도를 높이는 것과 압축률을 높이는 것은 같은 최적화다.

2. 아무도 가르치지 않았다. 2+3= 다음 토큰을 맞히지 못하면 손실을 지불하는데, 산술 규칙을 획득하면 그 손실이 줄어든다. 손실을 줄이라는 압박이 산술을 수단으로 만들어 낸 것이다.

3. 사전학습의 목표는 "유용한 답을 하는 것"이 아니라 "텍스트를 그럴듯하게 이어 쓰는 것"이기 때문이다. 인터넷에는 질문 뒤에 또 다른 질문이 이어지는 문서가 흔하므로, 질문을 이어 쓰는 것이 목표에 비추어 자연스러운 행동이다.

다음으로

다음 세션은 시간을 거슬러 올라간다. Transformer 이전에는 무엇으로 이 일을 했고, 왜 실패했는가. 실패의 이유를 알아야 Transformer의 설계가 필연으로 읽힌다.


Session 3언어 모델의 계보

n-gram은 문맥을 몇 단어까지밖에 못 봤고, RNN은 원리적으로는 무한히 볼 수 있었지만 실제로는 먼 정보를 잃어버렸으며 순차 계산 때문에 규모를 키울 수 없었다. Transformer는 이 두 문제를 모든 위치를 한 번에 서로 연결하는 방식으로 동시에 해결했다.

기술의 설계는 그것이 무엇을 해결하려고 했는지를 알 때 비로소 이해된다. Transformer의 구조를 외우지 않고 납득하려면, 그 앞에 무엇이 있었고 어디서 막혔는지를 먼저 봐야 한다.

n-gram: 문맥을 잘라 내는 방식

가장 단순한 언어 모델은 조회 테이블이다. 앞의 두 단어를 보고 다음 단어의 확률을 세어 둔다. "학교에 갔" 다음에 "다"가 몇 번 나왔는지를 코퍼스에서 집계해 저장하는 식이다. 이것이 n-gram 모델이고, 실제로 오랫동안 잘 작동했다.

문제는 문맥 길이를 늘리는 순간 드러난다. n을 3에서 5로 늘리면 가능한 문맥의 가짓수가 어휘 크기의 제곱만큼 늘어난다. 어휘가 5만이라면 3-gram에서 5-gram으로 갈 때 경우의 수가 25억 배가 된다. 대부분의 조합은 코퍼스에 한 번도 나타나지 않으므로 확률을 0으로 배정하게 되고, 실제로는 가능한 문장인데도 모델이 불가능하다고 판단하는 사태가 벌어진다. 이것을 희소성 문제라고 부른다. 여러 완화 기법이 나왔지만 근본적인 벽은 남았다. 문맥이 길어질수록 데이터가 기하급수적으로 부족해진다.

더 근본적인 한계도 있다. n-gram에는 단어들 사이의 유사성이라는 개념이 없다. "고양이"와 "고양이들"은 완전히 별개의 토큰이고, 한쪽에서 배운 것이 다른 쪽으로 전이되지 않는다. 단어가 서로 비슷할 수 있다는 발상, 즉 M3에서 다룰 임베딩이 여기서 빠져 있다.

RNN: 상태에 기억을 담는 방식

다음 세대는 완전히 다른 접근을 택했다. 문맥을 잘라 내는 대신, 지금까지 읽은 것을 고정 크기의 상태 벡터에 요약해서 들고 다니자는 것이다. 토큰을 하나 읽을 때마다 상태를 갱신하고, 다음 토큰 예측에는 그 상태를 쓴다. 원리적으로는 문맥 길이에 제한이 없다.

이 구조는 두 가지 문제를 안고 있었다.

첫째는 정보 손실이다. 아무리 긴 문맥이라도 고정 크기 벡터 하나에 눌러 담아야 한다. 100 단어 앞에 나온 인물의 이름은 그 사이 100번의 갱신을 거치면서 희미해진다. 수학적으로는 gradient가 시간축을 따라 반복 곱해지면서 기하급수적으로 작아지거나 커지는 문제로 나타나고, 그래서 기울기 소실이라고 부른다. LSTM과 GRU 같은 개선안이 이 문제를 상당히 완화했지만 없애지는 못했다.

둘째가 더 치명적이었다. 순차 계산이라는 구조 자체다. 위치 t의 상태를 계산하려면 위치 t−1의 상태가 먼저 있어야 한다. 문장 길이가 1000이면 1000번의 계산을 순서대로 해야 하고, 이 순서는 어떤 방법으로도 병렬화할 수 없다. GPU는 같은 연산을 수천 개 동시에 돌릴 때 힘을 내는 물건인데, RNN은 그 힘을 쓸 수 없는 구조였다. 데이터와 하드웨어가 커져도 모델을 그만큼 키울 수가 없었다.

정보가 이동하는 경로의 길이

RNN — 순차 전달, 경로 길이가 거리에 비례 t1t2t3 t4t5 t1의 정보가 t5에 닿으려면 네 단계 — 그사이 희미해진다 Attention — 모든 쌍을 직접 연결, 경로 길이는 항상 1 t1t2t3 t4t5 거리가 멀어도 한 번에 닿는다 — 대신 연결의 수가 T²로 늘어난다
RNN이 잃어버린 것은 정보가 이동하는 경로의 길이였다. Attention은 그 길이를 항상 1로 만든다. 대가는 연결의 개수인데, 토큰이 T개면 쌍이 T²개가 되어 계산량과 메모리가 함께 커진다. 이 맞바꿈이 M4 이후 모든 최적화의 출발점이다.

Transformer가 바꾼 것

2017년의 Transformer는 순차 전달을 아예 버렸다. 어떤 위치가 다른 위치의 정보를 필요로 하면, 중간을 거치지 않고 직접 가져온다. 이것이 attention이다.

이 결정으로 두 문제가 동시에 풀렸다. 정보가 이동하는 경로가 항상 한 단계이므로 거리에 따른 감쇠가 사라진다. 그리고 모든 위치의 계산이 서로 독립적이므로 전부 한꺼번에 돌릴 수 있다. GPU가 잘하는 형태의 계산, 즉 큰 행렬 곱셈 덩어리가 된 것이다. 규모를 키울 수 있게 된 순간이 바로 여기다.

다만 공짜는 아니었다. 위치들 사이의 순서 정보가 사라졌기 때문에 따로 넣어 줘야 하고(M3의 위치 인코딩), 모든 쌍을 계산하므로 비용이 길이의 제곱으로 자란다(M4의 비용 구조). 이후에 등장하는 거의 모든 기술은 이 제곱 비용을 어떻게 줄이거나 감당할 것인가에 대한 답이다. FlashAttention도, PagedAttention도, GQA도 전부 여기서 파생된 것이다.

백엔드의 언어로

세 세대의 차이는 참조를 어떻게 해결하는가의 차이로 읽을 수 있다. n-gram은 최근 몇 개만 보는 고정 크기 슬라이딩 윈도우다. RNN은 상태를 하나 들고 순차적으로 넘기는 누적 처리 파이프라인이다. Transformer는 모든 항목이 모든 항목을 조회할 수 있는 전역 조인에 가깝다. 그리고 익숙한 대로, 전역 조인은 표현력이 가장 크지만 비용이 크기의 제곱으로 자란다.

흔히 틀리는 지점

확인 질문

  1. n-gram에서 문맥을 늘리기 어려운 이유를 데이터 관점에서 설명해 보라.
  2. RNN의 두 가지 한계 중 규모 확장을 막은 쪽은 무엇이었고, 왜 그것이 더 치명적이었는가?
  3. Transformer가 얻은 것과 그 대가로 지불한 것을 각각 한 가지씩 말해 보라.
확인

1. 문맥 길이를 하나 늘릴 때마다 가능한 문맥 조합의 수가 어휘 크기만큼 곱해진다. 대부분의 조합은 코퍼스에 한 번도 나타나지 않으므로 확률이 0으로 배정되고, 실제로 가능한 문장을 불가능하다고 판단하게 된다.

2. 순차 계산 구조다. 위치 t의 계산이 t−1에 의존하므로 병렬화가 불가능했고, GPU의 성능을 쓸 수 없어 모델을 키울 수 없었다. 정보 손실은 LSTM 등으로 완화할 수 있었지만 이쪽은 구조를 바꾸지 않는 한 해결이 불가능했다.

3. 얻은 것은 임의의 두 위치를 한 단계로 연결하는 능력과 완전한 병렬 계산이다. 대가는 위치 쌍의 수가 T2이므로 계산량과 메모리가 길이의 제곱으로 자란다는 점, 그리고 순서 정보를 따로 주입해야 한다는 점이다.

다음으로

이제 전체 판이 어떻게 짜여 있는지를 볼 차례다. 사전학습, 사후학습, 추론, 응용, 평가가 서로 어떻게 맞물리는지 지도를 그린다.


Session 4이 분야의 지형도

LLM 시스템은 다섯 개의 층으로 나뉜다. 모델을 만드는 두 단계(사전학습, 사후학습), 그 모델을 돌리는 단계(추론과 서빙), 그 위에 얹는 단계(검색과 에이전트), 그리고 이 전부를 가로지르는 평가. 각 층에서 풀 수 있는 문제가 다르다는 것이 실무 판단의 핵심이다.

이 세션의 목적은 세부를 배우는 것이 아니라 서랍을 만드는 것이다. 앞으로 나올 모든 개념은 이 다섯 서랍 중 하나에 들어간다. 서랍이 있으면 새 개념을 만났을 때 어디에 넣을지 바로 알 수 있고, 문제를 만났을 때 어느 서랍을 열어야 하는지도 알 수 있다.

다섯 개의 층

다섯 개의 층과 각 층이 풀 수 있는 문제

M10 · M11M7 · M8 · M9 M6M5 응용 — 검색과 에이전트 추론과 서빙 사후학습 사전학습 지식·도구를 붙인다 빠르고 싸게 돌린다 행동을 바꾼다 능력이 정해진다 평가 M12 전 층을 가로지른다 아래로 갈수록 비싸고 느리게 바뀐다. 위로 갈수록 싸고 빠르게 바꿀 수 있다.
실무 판단의 기본 원칙은 가능한 한 위층에서 푸는 것이다. 위층은 배포 한 번으로 되돌릴 수 있지만, 아래층은 시간과 돈이 들고 되돌리기도 어렵다. "파인튜닝을 해야 하나"라는 질문을 받으면 먼저 그 문제가 정말로 아래층 문제인지부터 확인한다.

층을 고르는 감각

같은 증상이라도 원인이 어느 층에 있느냐에 따라 처방이 완전히 달라진다. 몇 가지 예를 보자.

모델이 우리 회사 제품 이름을 모른다면, 이것은 사전학습 데이터에 없었기 때문이다. 하지만 해결책은 사전학습이 아니라 응용층의 검색이다. 문서를 찾아서 컨텍스트에 넣어 주면 된다. 파인튜닝으로 지식을 주입하려는 시도가 대개 실패하는 이유는 층을 잘못 골랐기 때문이다.

모델이 답은 맞게 하는데 형식을 자꾸 어긴다면, 이것은 추론층에서 푸는 것이 가장 확실하다. 제약 디코딩으로 문법에 맞지 않는 토큰을 아예 못 고르게 막으면 형식 위반이 구조적으로 불가능해진다. 프롬프트로 부탁하는 것은 확률을 높일 뿐이지 보장하지 않는다.

응답이 느리다면 거의 항상 서빙층 문제다. 그런데 서빙층 안에서도 프롬프트를 읽는 구간이 느린 것인지 토큰을 뽑는 구간이 느린 것인지에 따라 처방이 다르고, 이 구분이 M7과 M9의 중심 주제다.

모델의 말투가 우리 서비스와 맞지 않는다면, 이것은 사후학습이 실제로 잘 듣는 몇 안 되는 문제다. 형식과 태도는 SFT가 잘 가르치는 영역이다.

백엔드의 언어로

이 그림은 익숙한 계층 구조와 성격이 같다. 아래층은 인프라이고 위층은 애플리케이션이다. 인프라를 바꾸는 것은 비싸고 느리고 영향 범위가 넓으며, 애플리케이션 변경은 싸고 빠르고 국소적이다. 문제가 생기면 먼저 애플리케이션에서 해결할 수 있는지 확인하고, 정말 아니면 아래로 내려간다.

한 가지 다른 점은 평가의 위치다. 일반 소프트웨어에서 테스트는 각 계층에 붙어 있지만, LLM 시스템에서는 평가가 독립적인 관심사로 떠오른다. 어떤 층을 바꾸든 출력이 확률적으로 변하기 때문에, "고쳤다"를 확인하는 일 자체가 별도의 공학이 된다.

확인 질문

  1. 다섯 개 층을 아래에서 위로 순서대로 말하고, 각 층에서 바꿀 수 있는 것을 한 마디로 붙여 보라.
  2. "모델이 우리 사내 규정을 모른다"는 문제를 사후학습이 아니라 응용층에서 푸는 것이 보통 나은 이유는?
  3. 평가가 특정 층에 속하지 않고 전체를 가로지르는 이유를 설명해 보라.
확인

1. 사전학습(능력이 정해진다) → 사후학습(행동을 바꾼다) → 추론과 서빙(빠르고 싸게 돌린다) → 응용(지식과 도구를 붙인다). 평가는 이 넷을 가로지른다.

2. 지식은 자주 바뀌고 출처를 밝혀야 하는 경우가 많은데, 파라미터에 넣으면 갱신할 때마다 재학습해야 하고 어디서 온 정보인지 추적할 수도 없다. 검색으로 붙이면 문서만 바꾸면 되고 근거도 함께 제시할 수 있다.

3. 어느 층을 바꾸든 최종 출력이 확률적으로 변하기 때문이다. 층별 단위 테스트만으로는 전체 품질의 변화를 알 수 없고, 시스템 수준에서 분포로 측정해야만 개선 여부를 판단할 수 있다.

다음으로

마지막으로, 이 분야에 들어올 때 백엔드 엔지니어가 반드시 넘어야 하는 사고의 문턱을 다룬다. 기술이 아니라 태도에 관한 세션이다.


Session 5백엔드 엔지니어의 사고 전환

지금까지의 정확성 기준은 "같은 입력에 같은 출력"이었다. LLM 시스템에서는 그 기준이 성립하지 않는다. 정확성, 테스트, 장애의 정의를 분포의 언어로 다시 세워야 한다.

이 세션에는 새로운 기술 용어가 거의 나오지 않는다. 그런데도 M0에서 가장 중요한 세션일 수 있다. 앞으로 만나게 될 실무 실수의 상당수는 지식의 부족이 아니라 이 사고 전환이 덜 된 데서 나오기 때문이다.

정확성이 확률이 될 때

익숙한 세계에서 함수는 결정적이다. 같은 입력에 항상 같은 출력을 내고, 그렇지 않으면 버그다. 테스트는 이 성질 위에 세워진다. 입력을 주고 기대값과 비교해서 같으면 통과, 다르면 실패다.

LLM은 이 전제를 깬다. 같은 프롬프트에 다른 답이 나오는 것은 정상이다. 온도를 0으로 낮춰도 완전히 결정적이 되지는 않는데, 배치 구성이나 부동소수점 연산 순서에 따라 미세한 차이가 생기고 그것이 토큰 선택을 뒤집을 수 있기 때문이다.

그래서 판정 기준이 바뀐다. "이 입력에 이 출력이 나오는가"가 아니라 "이런 종류의 입력들에 대해 우리가 원하는 성질을 만족하는 출력이 얼마나 자주 나오는가"를 물어야 한다. 단일 사례가 아니라 표본이 필요하고, 통과/실패가 아니라 비율이 나오며, 두 버전을 비교할 때는 그 비율 차이가 우연인지 아닌지를 따져야 한다. 이것이 M12에서 신뢰구간을 다루는 이유다.

여기서 곧바로 따라 나오는 실무 규칙이 하나 있다. 단일 사례로 결론 내리지 않는다. 프롬프트를 고쳤더니 한 케이스가 좋아졌다는 것은 아무 정보도 아니다. 확률적 시스템에서 한 번의 관측은 노이즈와 구별되지 않는다. 이 습관 하나만 잡아도 이 분야에서 낭비하는 시간이 크게 줄어든다.

실패의 모양이 달라진다

익숙한 장애는 대체로 이진적이다. 서버가 죽거나, 500을 뱉거나, 타임아웃이 난다. 감지가 쉽고 알람을 걸기도 쉽다.

LLM 시스템의 실패는 다르다. 응답은 200으로 돌아오고, 형식도 맞고, 문장도 자연스러운데, 내용이 틀렸다. 모니터링 지표는 전부 초록색인데 사용자는 잘못된 답을 받는다. 이런 실패는 시스템이 스스로 알려 주지 않는다. 별도로 측정하지 않으면 존재 자체를 모른다.

두 번째 차이는 실패가 점진적이라는 점이다. 인덱스에 문서가 조금씩 늘어나면서 검색 품질이 서서히 떨어지거나, 프롬프트에 조건이 하나씩 추가되면서 모델이 앞쪽 지시를 점점 무시하게 되는 식이다. 임계점이 없어서 언제 나빠졌는지 특정하기 어렵다. 그래서 회귀를 자동으로 탐지하는 장치를 미리 만들어 두는 것이 이 분야에서는 선택이 아니라 기본이다.

그럼에도 결정론은 포기하지 않는다

여기까지 읽으면 모든 것이 흐물흐물해진 것처럼 느껴질 수 있지만, 그렇지 않다. 오히려 정반대의 원칙이 성립한다.

출력 형식이 JSON이어야 한다면 모델에게 부탁하지 말고 문법으로 강제한다. 권한 검사는 모델에게 맡기지 말고 코드로 한다. 금액 계산은 모델이 아니라 함수가 한다. SQL을 만들게 하더라도 실행 전에 화이트리스트로 검증한다. 모델에게 맡기는 것은 판단이 필요하고 규칙으로 쓰기 어려운 부분뿐이고, 그 판단의 결과물조차 가능하면 결정론적 검증을 한 번 통과시킨다.

이 원칙은 M11에서 에이전트를 설계할 때 다시 나오지만, 사실은 지금부터 갖고 있어야 하는 태도다. LLM을 잘 쓰는 시스템은 LLM에게 많이 맡기는 시스템이 아니라, 맡길 곳과 맡기지 않을 곳을 정확히 나눈 시스템이다.

이미 갖고 있는 것들

전환해야 할 것이 있는 만큼, 그대로 쓸 수 있는 것도 많다. 이 점은 분명히 해 두는 편이 좋다.

측정하고 병목을 찾고 가설을 세워 검증하는 절차는 도메인과 무관하게 그대로 이전된다. 캐시의 무효화와 키 설계에 대한 감각은 M7의 KV cache와 M12의 캐싱 계층에서 거의 그대로 쓰인다. 요청 스케줄링과 동시성에 대한 이해는 M9의 서빙 엔진을 이해하는 데 결정적이다. 인덱스와 실행 계획을 읽어 본 경험은 M10의 근사 최근접 검색을 이해하는 데 그대로 쓰인다. 신뢰 경계를 긋는 감각은 M11의 프롬프트 인젝션 방어와 같은 문제다.

이 트랙에서 실제로 새로 배워야 하는 것은 모델 내부(M3–M4)와 학습(M5–M6), 그리고 GPU라는 하드웨어(M8) 세 덩어리다. 나머지 절반은 이미 아는 것 위에 이름표를 새로 붙이는 작업에 가깝다.

확인 질문

  1. "프롬프트를 고쳤더니 결과가 좋아졌다"는 보고를 받았을 때 가장 먼저 물어야 할 것은 무엇인가?
  2. LLM 시스템의 장애가 일반적인 서버 장애보다 감지하기 어려운 이유를 두 가지 말해 보라.
  3. 결정론적 코드와 확률적 모델의 경계를 긋는 원칙을 한 문장으로 말해 보라.
확인

1. 몇 건에서 확인했는지. 확률적 시스템에서 단일 사례는 노이즈와 구별되지 않으므로, 표본 크기와 비교 방식을 먼저 확인해야 한다.

2. 첫째, 응답은 정상(200, 올바른 형식, 자연스러운 문장)인데 내용만 틀리므로 시스템 지표에 잡히지 않는다. 둘째, 실패가 점진적이라 임계점이 없고 언제 나빠졌는지 특정하기 어렵다.

3. 규칙으로 쓸 수 있는 것은 전부 코드로 강제하고, 판단이 필요해 규칙화가 어려운 부분만 모델에 맡기되 그 출력도 가능하면 결정론적 검증을 통과시킨다.

모듈을 마치며

M0에서 세운 것은 세 개의 문장이다. 모델이 학습하는 것은 다음 토큰의 확률분포 하나뿐이라는 것, 능력은 그 목표를 극단적으로 밀어붙이는 과정에서 수단으로 얻어진다는 것, 그리고 시스템은 다섯 개의 층으로 나뉘며 문제를 어느 층에서 풀지를 고르는 것이 실무 판단이라는 것.

다음 모듈은 수학이다. 겁먹을 필요는 없다. 이 트랙에서 수학은 증명하거나 계산하기 위해 배우는 것이 아니라, 앞으로 나올 문장들을 정확히 읽기 위한 어휘로 배운다. softmax가 무엇인지 모르면 attention 설명은 그림으로만 남는다. M1은 그 어휘를 갖추는 열여섯 번의 이동이다.

M1세션 6–2116개

수학이라는 언어

증명하거나 계산하기 위해서가 아니라, 앞으로 나올 문장들을 정확히 읽기 위한 어휘로 수학을 배운다. 여기서 익힌 여섯 개의 기호가 이후 열한 개 모듈 전체에서 반복해 등장한다.

Session 6벡터와 내적

두 벡터의 내적은 대응하는 성분끼리 곱해서 전부 더한 값이다. 이 단순한 연산이 "둘이 얼마나 같은 방향을 보는가" 를 재는 도구가 되고, LLM 안에서 유사도가 필요한 거의 모든 자리에 이것이 들어간다.

왜 벡터인가

컴퓨터에게 "학교"라는 단어를 이해시키려면 먼저 숫자로 바꿔야 한다. 가장 순진한 방법은 어휘에 번호를 붙이는 것이다. 학교는 4712번, 회사는 8391번 하는 식으로. 그런데 이 방식에는 치명적인 문제가 있다. 4712와 8391 사이의 산술적 관계에는 아무 의미가 없다. 두 숫자를 빼거나 더해도 의미 있는 무언가가 나오지 않고, 번호가 가깝다고 해서 뜻이 비슷하지도 않다.

그래서 단어 하나를 숫자 하나가 아니라 숫자의 묶음으로 표현한다. 이 묶음이 벡터다. 예를 들어 4차원으로 표현한다면 학교는 [0.8, 0.1, 0.6, -0.2], 회사는 [0.7, 0.2, 0.5, -0.1] 같은 식이다. 실제 모델에서는 차원이 수천 개지만 원리는 같다. 이렇게 하면 각 차원이 의미의 어떤 축을 담당하게 되고, 두 벡터가 비슷하다는 것이 두 단어가 비슷하다는 뜻이 될 수 있다.

여기서 중요한 것은 각 차원이 무엇을 뜻하는지 사람이 정하지 않는다는 점이다. 학습 과정에서 저절로 정해지고, 대개 사람이 이름 붙일 수 있는 축이 아니다. 우리가 알 수 있는 것은 결과적으로 비슷한 것끼리 가까워졌다는 사실뿐이다.

내적이 하는 일

두 벡터 ab의 내적은 이렇게 계산한다.

a·b=a1b1+a2b2+a3b3++anbn

성분끼리 곱해서 전부 더한다. 그게 전부다. 결과는 벡터가 아니라 숫자 하나다.

작은 예로 감을 잡아 보자. a=[3,1], b=[2,4]라면 내적은 3×2+1×4=10이다. 이번에는 b[-2, 6]으로 바꿔 보자. 내적은 3×(2)+1×6=0이 된다. 마지막으로 b=[3,1]이면 내적은 91=10이다.

세 결과가 각각 무엇을 말하는지가 핵심이다. 내적이 크게 양수면 두 벡터가 같은 방향을 향한다. 0이면 서로 직각이다. 음수면 반대 방향이다. 실제로 마지막 예의 b=[3,1]a=[3,1]을 정확히 뒤집은 것이다.

이유는 성분별로 보면 분명하다. 어떤 차원에서 두 벡터가 둘 다 큰 양수면 그 항의 곱이 크게 양수가 되어 합에 기여한다. 한쪽이 양수고 한쪽이 음수면 곱이 음수가 되어 합을 깎는다. 즉 내적은 차원마다 "둘이 같은 쪽으로 치우쳤는가"를 채점하고 전부 합산한 점수다.

내적의 부호가 말하는 것

같은 쪽직각반대 쪽 a·b > 0 a·b = 0 a·b < 0 내적은 방향의 일치도를 하나의 숫자로 요약한다
내적은 크기와 방향을 함께 반영한다. 방향이 같아도 벡터가 길면 값이 커진다. 이 성질이 때로는 문제가 되는데, 그것을 떼어 내는 방법이 다음 두 세션의 주제다.

LLM 안에서 내적이 나오는 자리

내적은 이 트랙 전체에서 세 번 결정적으로 등장한다.

백엔드의 언어로

내적을 두 레코드의 필드별 점수를 매겨 합산하는 랭킹 함수로 보면 편하다. 각 차원이 하나의 시그널이고, 두 항목이 그 시그널에서 같은 방향으로 강하면 점수가 오른다. 검색 시스템에서 여러 시그널을 가중합해 최종 점수를 내는 것과 형태가 같다.

다만 차이가 있다. 검색 시스템의 시그널은 사람이 설계하고 이름이 있다. 임베딩의 차원은 학습으로 생겼고 이름이 없다. 그래서 "이 문서가 왜 상위에 왔는가"를 차원 단위로 설명하는 일이 원리적으로 어렵다. 이 설명 불가능성은 M10에서 검색 품질을 디버깅할 때 실제 제약으로 나타난다.

흔히 틀리는 지점

확인 질문

  1. [2, -1, 3][1, 4, 0]의 내적을 계산하고, 두 벡터의 관계를 한 마디로 말해 보라.
  2. 단어를 숫자 하나가 아니라 벡터로 표현하는 이유는?
  3. 내적이 attention에서 어떤 질문에 답하는 데 쓰이는가?
확인

1. 2×1+(1)×4+3×0=24+0=2. 음수이므로 두 벡터는 전체적으로 서로 반대 방향으로 조금 치우쳐 있다. 다만 절댓값이 작으므로 거의 무관에 가깝다.

2. 숫자 하나로는 단어들 사이의 유사성을 표현할 수 없기 때문이다. 벡터로 두면 여러 축에서 비슷한 정도를 표현할 수 있고, 벡터 사이의 거리나 각도가 의미의 유사도가 될 수 있다.

3. "이 토큰이 저 토큰을 얼마나 참조해야 하는가"에 답한다. 두 토큰의 벡터를 내적해 관련도 점수를 만들고, 그 점수가 참조 비율을 결정한다.


Session 7노름과 정규화

노름은 벡터의 길이이고, 정규화는 그 길이를 1로 맞춰 방향만 남기는 조작이다. 크기가 의미를 왜곡할 때 이것을 걷어내면 비교가 공정해진다.

길이를 재는 법

벡터 [3, 4]의 길이는 얼마인가. 원점에서 그 점까지의 거리를 재면 되므로 피타고라스 정리로 +=5다. 차원이 늘어나도 똑같다. 각 성분을 제곱해서 더하고 제곱근을 씌운다. 이것을 L2 노름이라고 부르고 a로 쓴다.

a=a12+a22++an2

여기서 유용한 관찰이 하나 있다. 벡터를 자기 자신과 내적하면 a·a=a12+a22+가 되는데, 이것은 노름의 제곱이다. 즉 a=a·a다. 길이와 내적은 별개의 개념이 아니라 같은 연산의 두 얼굴이다.

정규화가 하는 일

벡터를 자기 길이로 나누면 길이가 정확히 1인 벡터가 된다. 방향은 그대로고 크기만 표준화된 것이다.

â=a/a

[3, 4]를 정규화하면 [0.6, 0.8]이 된다. 여전히 같은 방향을 가리키지만 길이가 1이다.

왜 이런 조작이 필요한가. 문서 검색을 예로 들면 분명해진다. 긴 문서일수록 임베딩 벡터의 길이가 커지는 경향이 있다고 해 보자. 이때 내적으로만 순위를 매기면 내용과 무관하게 긴 문서가 항상 상위에 온다. 길이가 점수를 지배해 버리는 것이다. 벡터를 정규화하면 이 편향이 사라지고 순수하게 방향, 즉 의미의 유사성만 남는다.

정규화가 만들어 내는 등식

이 부분은 M10에서 벡터 데이터베이스를 다룰 때 실제 설정값에 직접 영향을 주므로 정확히 기억해 둘 가치가 있다.

두 벡터가 모두 정규화되어 있으면, 내적과 코사인 유사도가 같은 값이 된다. 그리고 L2 거리는 내적의 단조 감소 함수가 된다. 구체적으로 ab2=a2+b22(a·b)=22(a·b)이므로, 내적이 클수록 거리가 작아진다.

기억해 둘 결론

벡터를 L2 정규화해 두면 내적으로 정렬한 순위와 코사인으로 정렬한 순위와 L2 거리로 정렬한 순위가 전부 같아진다. 그래서 실무에서는 임베딩을 저장할 때 미리 정규화해 두고, 인덱스는 계산이 가장 싼 내적을 쓰는 경우가 많다. 반대로 정규화를 빼먹고 코사인 인덱스를 쓰면 검색 결과가 조용히 망가진다.

LLM 안에서 정규화가 나오는 자리

임베딩 검색에서 벡터를 저장하기 전에 정규화하는 것이 첫 번째 자리다. 두 번째는 M2에서 볼 LayerNorm과 RMSNorm인데, 이쪽은 벡터의 방향을 보존하려는 목적이 아니라 각 층을 지나면서 값의 크기가 폭주하거나 소멸하지 않게 잡아 주려는 목적이다. 같은 "정규화"라는 말을 쓰지만 의도가 다르다는 점은 구별해 두는 편이 좋다.

백엔드의 언어로

여러 시그널을 합쳐 순위를 매길 때 스케일이 다른 항목을 그대로 더하면 스케일이 큰 항목이 결과를 지배한다. 그래서 각 시그널을 0에서 1 사이로 맞춘 뒤 합치는데, 정규화가 하는 일이 정확히 그것이다. 차이는 여기서 표준화하는 대상이 개별 시그널이 아니라 벡터 전체의 길이라는 점이다.

흔히 틀리는 지점

확인 질문

  1. [0, 5, 0]을 정규화하면 무엇이 되는가?
  2. 정규화하지 않은 벡터로 내적 검색을 하면 어떤 편향이 생길 수 있는가?
  3. 임베딩을 미리 정규화해 두면 실무에서 무엇이 편해지는가?
확인

1. 길이가 25=5이므로 각 성분을 5로 나눠 [0, 1, 0]이 된다.

2. 길이가 긴 벡터가 방향과 무관하게 높은 점수를 받는다. 예컨대 긴 문서의 임베딩이 길어지는 경향이 있다면, 내용과 상관없이 긴 문서가 상위를 차지한다.

3. 내적·코사인·L2 거리로 매긴 순위가 모두 일치하게 되므로 인덱스 종류를 바꿔도 결과가 달라지지 않고, 계산이 가장 싼 내적을 안심하고 쓸 수 있다.


Session 8코사인 유사도

코사인 유사도는 두 벡터가 이루는 각도의 코사인 값이고, 계산으로는 내적을 두 길이의 곱으로 나눈 것이다. 값은 항상 −1과 1 사이에 놓이며, 임베딩 검색에서 사실상 표준 척도로 쓰인다.

정의와 직관

cos(a,b)=(a·b)/(ab)

앞 두 세션을 이어 붙인 것이 전부다. 내적은 방향의 일치도를 재지만 길이의 영향을 받는다. 그래서 두 길이로 나눠 그 영향을 제거한다. 결과는 −1에서 1 사이의 값이고, 1이면 완전히 같은 방향, 0이면 직각, −1이면 정반대다.

수치로 감을 잡아 보자. 검색 시스템에서 질의와 문서의 코사인 유사도가 보통 어떤 값을 갖는지가 실무에서 자주 필요한데, 대략 이런 범위다. 0.9 이상이면 거의 같은 내용이거나 중복 문서다. 0.7에서 0.85는 관련도가 높은 문서, 0.5에서 0.7은 주제는 겹치지만 답을 담고 있지 않을 수 있는 문서, 0.3 이하면 대체로 무관하다. 다만 이 숫자는 임베딩 모델마다 크게 다르다. 어떤 모델은 무관한 문서에도 0.6을 주고 어떤 모델은 0.2를 준다. 그래서 절대적인 임계값을 다른 시스템에서 그대로 가져오면 안 되고, 반드시 자기 데이터에서 분포를 보고 정해야 한다.

왜 각도인가

의미가 방향에 실린다는 발상이 코사인 유사도의 전제다. 벡터의 길이는 대개 그 단어나 문서가 얼마나 자주 등장했는지, 텍스트가 얼마나 길었는지 같은 요인에서 오고, 의미와는 상관이 적다. 그래서 길이를 버리고 방향만 남긴다.

이것이 항상 옳은 것은 아니다. 어떤 임베딩 모델은 길이에 신뢰도 같은 정보를 싣기도 하고, 그런 경우에는 내적이 더 나은 척도가 된다. 그래서 모델 카드에 어떤 척도를 쓰라고 명시되어 있으면 그것을 따르는 것이 원칙이다.

코사인 유사도가 놓치는 것

한 가지 함정을 알아 두면 M10에서 시간을 아낄 수 있다. 코사인 유사도가 높다는 것은 주제가 비슷하다는 뜻이지, 답이 들어 있다는 뜻이 아니다.

"우리 회사의 연차 규정은 어떻게 되나요"라는 질문에 대해, 연차 규정을 상세히 설명한 문서와 "연차 규정 개정 안내(첨부 파일 참조)"라는 공지문은 코사인 유사도가 비슷하게 나올 수 있다. 어휘가 겹치고 주제가 같기 때문이다. 하지만 후자에는 답이 없다.

이 간극이 M10에서 리랭커가 필요한 이유다. 임베딩 검색은 주제 유사도로 후보를 좁히는 데까지만 잘하고, "이 문서에 실제로 답이 있는가"는 질문과 문서를 함께 읽는 별도의 모델이 판단해야 한다.

백엔드의 언어로

코사인 유사도는 스케일 불변 유사도 지표다. 두 시계열이 얼마나 같은 모양으로 움직이는지 볼 때 절대값을 무시하고 형태만 비교하는 것과 성격이 같다. 데이터베이스로 치면, 값 자체가 아니라 값들의 상대적 패턴으로 매칭하는 것이다.

흔히 틀리는 지점

확인 질문

  1. 이미 정규화된 두 벡터에 대해 코사인 유사도를 구하려면 무엇을 하면 되는가?
  2. 코사인 유사도가 0.82인 문서가 질문의 답을 담고 있지 않을 수 있는 이유는?
  3. 다른 팀이 쓰는 유사도 임계값 0.75를 그대로 가져오면 안 되는 이유는?
확인

1. 그냥 내적을 하면 된다. 두 벡터의 길이가 1이므로 분모가 1이 되어 내적이 곧 코사인 값이다.

2. 코사인 유사도는 주제와 어휘의 유사성을 재는 것이지 답의 존재를 재는 것이 아니다. 같은 주제를 다루지만 결론이 없는 문서도 높은 값을 받을 수 있다.

3. 유사도의 절대 스케일은 임베딩 모델마다 다르다. 같은 관련도라도 모델에 따라 0.6이 나오기도 하고 0.9가 나오기도 하므로, 자기 데이터와 모델에서 분포를 확인해 정해야 한다.


Session 9행렬곱은 선형변환이다

행렬을 벡터에 곱하는 것은 벡터를 다른 공간으로 옮기는 일이다. 신경망의 모든 층이 하는 기본 동작이 바로 이것이고, 층을 쌓는다는 말은 이 변환을 연달아 적용한다는 뜻이다.

계산 규칙보다 의미부터

행렬 W와 벡터 x를 곱해 y=Wx를 얻는다고 할 때, 계산 규칙은 단순하다. y의 첫 성분은 W의 첫 행과 x의 내적, 둘째 성분은 둘째 행과 x의 내적, 이런 식이다. 즉 행렬곱은 내적을 여러 번 하는 것이고, 앞 세션에서 배운 것이 그대로 재사용된다.

여기서 shape이 어떻게 되는지가 중요하다. W[m, n] 모양이고 x가 길이 n인 벡터라면 결과는 길이 m인 벡터다. 즉 행렬곱은 n차원 벡터를 m차원 벡터로 옮긴다. W의 행 개수가 출력 차원, 열 개수가 입력 차원이다. 이 규칙 하나만 확실히 잡아 두면 M4에서 텐서 모양을 추적할 때 헤매지 않는다.

의미로 보면 이렇다. W의 각 행은 하나의 질문지다. "입력이 이런 패턴을 얼마나 갖고 있는가"를 묻는 벡터이고, 그 행과 입력의 내적이 그 질문에 대한 점수다. 행이 m개면 m가지 질문을 던지는 것이고, 출력 벡터는 그 답을 모은 것이다.

왜 이것이 신경망의 전부에 가까운가

신경망의 기본 단위인 선형 계층은 y=Wx+b다. W가 행렬, b가 편향 벡터다. Transformer 안에서 파라미터를 가진 부분은 거의 전부 이 형태다. Q, K, V를 만드는 것도 행렬곱이고, 어텐션 출력을 다시 섞는 것도, FFN의 두 층도, 마지막에 어휘 크기로 펼치는 것도 전부 행렬곱이다.

그래서 모델의 파라미터 수를 세는 일도 결국 행렬의 크기를 세는 일이 된다. [m, n] 행렬의 파라미터는 m×n개다. 이것이 M4에서 "모델 크기를 손으로 계산한다"고 할 때 하는 일의 전부다.

계산 비용도 여기서 나온다. [m, n] 행렬과 길이 n 벡터의 곱은 곱셈 m×n번과 덧셈 그만큼을 필요로 한다. 그래서 흔히 곱셈-덧셈 한 쌍을 2 FLOPs로 세어 2mn 이라고 계산한다. 파라미터 하나당 2 FLOPs라는 규칙이 여기서 나오고, M8에서 GPU 성능을 따질 때 이 규칙을 계속 쓴다.

왜 활성함수가 필요해지는가

행렬곱만 계속 쌓으면 어떻게 될까. W2(W1x)는 결합법칙에 의해 (W2W1)x와 같고, W2W1은 또 하나의 행렬이다. 즉 선형 변환을 아무리 여러 번 겹쳐도 결국 하나의 선형 변환과 똑같다. 층을 백 개 쌓아도 표현력이 한 층과 같다는 뜻이다.

이 사실이 활성함수의 존재 이유다. 층 사이에 비선형 함수를 하나 끼워 넣으면 이 붕괴가 일어나지 않고, 층을 쌓는 것이 실제로 표현력을 늘린다. M2에서 다시 다루지만, 논리의 출발점은 여기다.

백엔드의 언어로

행렬곱을 일괄 변환 파이프라인의 한 단계로 보면 자연스럽다. 입력 레코드의 필드 집합을 받아서, 미리 정해진 규칙에 따라 다른 필드 집합으로 옮긴다. 규칙이 곧 행렬이고, 그 규칙을 데이터에서 학습한다는 점만 다르다.

한 가지 강조할 점은 이 연산이 GPU가 가장 잘하는 형태라는 것이다. 큰 행렬곱은 수천 개의 독립적인 곱셈-덧셈으로 쪼개지고, 그 각각을 동시에 돌릴 수 있다. Transformer가 규모를 키울 수 있었던 이유가 바로 계산의 대부분을 큰 행렬곱 덩어리로 만들었기 때문이다. M8에서 이 이야기를 하드웨어 쪽에서 다시 만난다.

흔히 틀리는 지점

확인 질문

  1. [512, 2048] 모양의 행렬은 몇 차원 벡터를 몇 차원으로 옮기며, 파라미터는 몇 개인가?
  2. 행렬곱을 내적의 언어로 다시 설명해 보라.
  3. 활성함수 없이 선형 계층만 열 개 쌓으면 왜 한 개 쌓은 것과 같은가?
확인

1. 2048차원 벡터를 512차원으로 옮긴다. 행이 출력 차원, 열이 입력 차원이다. 파라미터는 512×2048=1,048,576개이고 편향까지 포함하면 512개가 더 붙는다.

2. 출력 벡터의 i번째 성분은 행렬의 i번째 행과 입력 벡터의 내적이다. 행렬의 각 행이 하나의 질문이고, 내적이 그 질문에 대한 점수다.

3. 선형 변환의 합성은 다시 선형 변환이기 때문이다. 열 개의 행렬을 미리 곱해 두면 하나의 행렬이 되고, 그 하나로 계산한 결과와 완전히 같다.


Session 10랭크와 저랭크 근사

행렬의 랭크는 그 행렬이 실제로 쓰고 있는 독립적인 방향의 개수다. 큰 행렬이라도 랭크가 낮으면 훨씬 작은 두 행렬의 곱으로 거의 그대로 복원할 수 있고, 이 사실이 M6의 LoRA를 가능하게 한다.

행렬이 크다고 내용이 많은 것은 아니다

[1000, 1000] 행렬이 있다고 하자. 숫자가 백만 개다. 그런데 만약 이 행렬의 모든 행이 어떤 하나의 벡터의 상수배라면, 이 행렬이 담고 있는 정보는 사실상 그 벡터 하나와 배수 천 개뿐이다. 숫자 백만 개를 저장했지만 실제 내용은 이천 개 남짓인 것이다. 이런 행렬을 랭크가 1이라고 말한다.

일반화하면 이렇다. 행렬의 랭크는 서로 독립인 행(또는 열)의 최대 개수다. [m, n] 행렬의 랭크는 최대 min(m,n)이지만, 실제 데이터에서 나오는 행렬은 그보다 훨씬 낮은 경우가 많다. 행들 사이에 강한 상관이 있으면 랭크가 떨어진다.

저랭크 분해

랭크가 r[m, n] 행렬은 [m, r] 행렬과 [r, n] 행렬의 곱으로 정확히 쓸 수 있다. 랭크가 정확히 r이 아니더라도, 가장 중요한 r개의 방향만 남기면 원래 행렬을 상당히 잘 근사할 수 있다. 이것이 저랭크 근사다.

절약되는 양을 세어 보면 왜 이것이 중요한지 보인다. m=n=4096이고 r=8이라면, 원래 행렬은 파라미터가 4096×4096=16,777,216개다. 분해하면 4096×8+8×4096=65,536개다. 256분의 1이다.

큰 행렬을 두 개의 얇은 행렬로

ΔW 4096×4096 16.8M B 4096×8 × A 8×4096 65.5K — 256분의 1 r이 작을수록 더 아끼지만 표현력도 준다
LoRA는 사전학습된 가중치 W를 건드리지 않고, 학습으로 생기는 변화량 ΔW만 이렇게 분해해서 학습한다. 이 그림의 BA만 갱신하면 되므로 학습에 필요한 메모리가 급감한다.

이 가정이 성립하는 이유와 깨지는 지점

LoRA의 전제는 "모델을 새 과제에 적응시킬 때 가중치의 변화량은 저랭크에 가깝다"는 것이다. 사전학습으로 이미 풍부한 표현을 갖춘 모델을 특정 스타일이나 형식에 맞추는 정도의 조정이라면, 그 조정이 몇 개의 방향으로 설명된다는 관찰이다. 실험적으로 잘 맞는 것으로 확인되었고, 그래서 오늘날 파인튜닝의 기본값이 되었다.

중요한 것은 이 전제가 언제 깨지는지다. 적응의 폭이 클수록 랭크 가정이 성립하지 않는다. 영어로 학습된 모델을 완전히 다른 언어로 옮기거나, 일반 텍스트 모델을 전문 도메인의 새로운 개념 체계에 적응시키는 경우가 그렇다. 이럴 때는 랭크를 올리거나, 전체 파인튜닝을 하거나, 아예 계속학습을 고려해야 한다. M6에서 이 판단 기준을 다시 정리한다.

백엔드의 언어로

저랭크 근사는 손실 압축이다. 데이터의 주된 변동을 설명하는 소수의 요인만 남기고 나머지를 버린다. 추천 시스템의 행렬 분해나 차원 축소와 정확히 같은 발상이다.

한 가지 유용한 감각은 r이 압축률을 조절하는 손잡이라는 점이다. r을 키우면 원본에 가까워지지만 절약이 줄고, 줄이면 절약이 커지지만 표현력이 준다. M6에서 LoRA의 rank를 4에서 64까지 바꿔 가며 고르는 일이 이 손잡이를 돌리는 것이다.

확인 질문

  1. 모든 행이 서로의 상수배인 [100, 100] 행렬의 랭크는 얼마인가?
  2. [2048, 2048] 가중치를 rank 16으로 분해하면 파라미터가 몇 배로 줄어드는가?
  3. LoRA의 저랭크 가정이 깨지는 상황을 하나 들어 보라.
확인

1. 1이다. 독립적인 행이 하나뿐이므로 나머지는 전부 그 하나로 설명된다.

2. 원래는 2048×2048=4,194,304개, 분해하면 2048×16+16×2048=65,536개다. 정확히 64분의 1이다.

3. 적응의 폭이 큰 경우다. 예를 들어 모델이 거의 다루지 않은 언어로 옮기거나, 사전학습에 없던 전문 도메인의 개념 체계를 새로 넣어야 하는 경우에는 변화량이 소수의 방향으로 설명되지 않는다.


Session 11확률분포와 조건부확률

LLM의 출력은 P(다음토큰|지금까지의토큰)이라는 조건부 확률분포다. 이 표기 하나를 정확히 읽을 수 있으면 이후 나오는 손실 함수, 샘플링, 선호 최적화의 수식이 전부 같은 문법으로 읽힌다.

분포라는 말의 정확한 뜻

확률분포는 가능한 결과 각각에 확률을 배정한 것이다. 조건은 두 가지뿐이다. 모든 확률이 0 이상이어야 하고, 전부 더하면 1이어야 한다.

주사위라면 결과가 여섯 가지이고 각각 1/6이다. LLM이라면 결과가 어휘 전체, 즉 10만 가지쯤 되고 각각에 확률이 배정된다. 크기만 다를 뿐 구조는 같다.

여기서 M0에서 말한 내용이 다시 확인된다. 모델은 "다음 토큰은 학교다"라고 말하지 않는다. "학교 0.31, 회사 0.22, 집 0.18, ... " 하는 식으로 10만 개의 숫자를 내놓는다. 하나를 고르는 일은 그다음에 별도로 이루어지는 선택이고, 그 선택 방법이 M7의 샘플링이다.

조건부확률과 세로 막대

P(A|B)는 "B가 주어졌을 때 A의 확률"이라고 읽는다. 세로 막대는 나눗셈이 아니라 "~이 주어졌을 때"라는 조건을 표시하는 기호다. 이 기호를 나눗셈으로 오독하는 경우가 의외로 많으니 한 번 짚어 둔다.

언어 모델은 이렇게 쓴다.

P(x_t | x_1, x_2, ..., x_{t-1})

앞의 토큰들이 전부 주어졌을 때 다음 토큰 x_t의 분포. 이것이 모델이 계산하는 대상의 정확한 표기다.

문장 전체의 확률

한 문장의 확률은 각 위치의 조건부확률을 전부 곱한 것이다.

P(문장)=P(x1)×P(x2|x1)×P(x3|x1,x2)×

이것을 연쇄 법칙이라고 부르는데, 확률의 정의에서 곧바로 따라 나오는 항등식이라 특별한 가정이 필요 없다. 중요한 것은 이 식이 긴 문장의 확률은 필연적으로 아주 작은 수가 된다는 사실을 말해 준다는 점이다. 0.3 정도의 값을 100번 곱하면 10의 마이너스 50승 수준이 된다. 부동소수점으로는 다룰 수 없는 크기다.

그래서 실제로는 확률을 곱하는 대신 로그 확률을 더한다. 곱셈이 덧셈이 되고 언더플로가 사라진다. 이것이 다음 세션들에서 로그가 계속 등장하는 실용적인 이유이고, M7에서 생성 결과의 신뢰도를 볼 때 확률이 아니라 logprob을 보는 이유이기도 하다.

자기회귀라는 이름

P(x_t | x_{<t})를 순서대로 적용해 토큰을 하나씩 만들어 나가는 방식을 자기회귀 생성이라고 부른다. 만든 토큰을 다시 입력에 넣어 다음 토큰의 조건으로 쓰기 때문에 "자기"이고, 과거를 조건으로 삼기 때문에 "회귀"다.

이 구조에서 한 가지 성질이 나온다. 한 번 뱉은 토큰은 되돌릴 수 없다. 잘못된 토큰이 나와도 그것이 다음 토큰의 조건이 되어 버리므로, 초반의 실수가 뒤로 갈수록 증폭될 수 있다. 모델이 한 번 잘못된 전제를 세우면 그 위에서 그럴듯한 문장을 계속 이어 가는 현상의 구조적 원인이 여기 있다.

백엔드의 언어로

조건부확률은 상태에 의존하는 응답 분포다. 같은 요청이라도 세션 상태에 따라 다른 응답이 나가는 시스템을 떠올리면 된다. 다만 여기서 상태는 지금까지 생성된 토큰 전체이고, 그 상태가 매 스텝 자라난다.

자기회귀 생성은 이전 출력이 다음 입력이 되는 피드백 루프다. 이런 루프는 오류가 누적되기 쉬운 구조이고, 그래서 M11에서 에이전트를 설계할 때 중간에 결정론적 검증을 끼워 넣는 것이 중요해진다.

확인 질문

  1. P(A|B)를 소리 내어 읽어 보라. 세로 막대는 무슨 뜻인가?
  2. 긴 문장의 확률을 직접 곱해서 계산하면 왜 문제가 되고, 실무에서는 어떻게 피하는가?
  3. 자기회귀 생성에서 초반의 잘못된 토큰이 특히 위험한 이유는?
확인

1. "B가 주어졌을 때 A의 확률". 세로 막대는 나눗셈이 아니라 조건을 표시하는 기호다.

2. 1보다 작은 수를 수십, 수백 번 곱하면 부동소수점의 표현 범위 아래로 내려가 언더플로가 난다. 그래서 확률을 곱하는 대신 로그 확률을 더한다.

3. 생성된 토큰이 그다음 토큰의 조건이 되기 때문이다. 초반의 잘못된 토큰이 이후 모든 예측의 전제가 되어 오류가 증폭되고, 모델은 그 잘못된 전제 위에서 일관되게 그럴듯한 문장을 이어 간다.


Session 12기댓값과 분산

기댓값은 확률로 가중한 평균이고, 분산은 값들이 평균에서 얼마나 흩어져 있는지를 잰다. 이 두 개념은 M4에서 왜 attention 점수를 d로 나누는가에 답할 때 결정적으로 쓰인다.

기댓값

기댓값은 각 결과에 그 확률을 곱해서 더한 것이다. 주사위라면 1×(1/6)+2×(1/6)++6×(1/6)=3.5다.

LLM 맥락에서 기댓값이 등장하는 자리는 손실 함수다. 학습에서 최소화하는 것은 하나의 예제에 대한 손실이 아니라 데이터 분포 전체에 대한 손실의 기댓값이다. 실제로는 분포 전체를 볼 수 없으니 배치에서 뽑은 표본의 평균으로 근사하는데, 이것이 미니배치 학습의 논리적 근거다.

기댓값의 성질 중 하나만 기억해 두면 유용하다. 기댓값은 덧셈에 대해 선형이다.E[X+Y]=E[X]+E[Y]가 항상 성립하고, X와 Y가 독립인지 아닌지와 무관하다.

분산

분산은 값이 평균에서 얼마나 벗어나는지의 제곱을 평균한 것이다.

Var(X)=E[(XE[X])2]

제곱하는 이유는 부호를 없애기 위해서다. 그냥 차이를 평균하면 위아래로 상쇄되어 항상 0이 된다. 분산의 제곱근이 표준편차이고, 이쪽은 원래 값과 단위가 같아 해석이 쉽다.

분산에는 기댓값과 다른 중요한 성질이 있다. 독립인 확률변수들의 합의 분산은 각 분산의 합이다.

Var(X+Y)=Var(X)+Var(Y) (X, Y가 독립일 때)

이 조건부 성질이 다음 이야기의 열쇠다.

d가 나오는 자리

이제 M4를 미리 한 걸음 밟아 보자. Attention에서는 두 벡터 qk의 내적을 계산한다. 두 벡터의 차원을 d라고 하자.

q·k=q1k1+q2k2++qdkd

각 항 qiki가 평균 0, 분산 1 정도의 값이라고 가정하자. 학습 초기에 초기화를 그렇게 맞추기 때문에 대략 맞는 가정이다. 항이 서로 독립이라면, 위의 성질에 의해 합의 분산은 항의 개수만큼, 즉 d가 된다. 표준편차는 d다.

d가 128이라면 표준편차가 약 11.3이다. 내적 값이 대략 −34에서 34 정도의 범위에서 출렁인다는 뜻이다. 이 값들을 그대로 softmax에 넣으면 어떻게 될까. 다음 세션들에서 보겠지만, softmax는 입력의 차이가 커질수록 출력이 한쪽으로 극단적으로 쏠린다. 차이가 30이면 사실상 1등에 확률 전부가 가고 나머지는 0이 된다.

그러면 두 가지가 망가진다. 첫째, 여러 토큰의 정보를 섞는다는 attention의 취지가 사라지고 하나만 보게 된다. 둘째, 더 심각하게, softmax의 출력이 0이나 1로 포화되면 기울기가 거의 0이 되어 학습이 멈춘다.

해결책은 간단하다. 내적 결과를 d로 나눈다. 분산이 d인 값을 d로 나누면 분산이 다시 1이 된다. 이제 점수가 적당한 범위에 있고 softmax가 부드럽게 동작한다.

M4를 위한 예습

"왜 d로 나누는가"는 attention에 관한 가장 흔한 면접 질문이고, 답은 전적으로 이 세션의 분산 이야기다. 내적의 분산이 차원에 비례해 커지므로, 표준편차인 d로 나눠 분산을 1로 되돌리는 것이다. 그러지 않으면 softmax가 포화되어 기울기가 사라진다.

백엔드의 언어로

분산은 지표의 변동성이다. 응답 시간의 평균만 보면 안 되고 분산이나 p95를 함께 보는 것과 같은 감각이다. 그리고 여기서 배운 "독립인 항을 많이 더하면 변동이 항의 개수에 비례해 커진다"는 성질은, 여러 독립적인 지연 요소가 누적되는 시스템의 꼬리 지연이 왜 커지는지를 설명하는 것과 같은 수학이다.

확인 질문

  1. 독립인 확률변수 100개를 더하면 분산은 어떻게 되는가? 표준편차는?
  2. 차원이 512인 두 벡터의 내적은 대략 어느 정도의 표준편차를 갖는가?
  3. d로 나누지 않으면 학습이 왜 멈추는지 두 단계로 설명해 보라.
확인

1. 각 분산이 1이라면 합의 분산은 100이 된다. 표준편차는 그 제곱근인 10이다.

2. 각 항의 분산이 1이라면 합의 분산은 512, 표준편차는 51222.6이다.

3. 첫째, 내적 값의 범위가 커져 softmax 입력의 차이가 벌어지고, 출력이 사실상 0과 1로 포화된다. 둘째, softmax가 포화된 지점에서는 기울기가 거의 0이라 역전파로 전달되는 학습 신호가 사라진다.


Session 13log와 exp

로그는 곱셈을 덧셈으로 바꾸고 아주 작은 수를 다룰 수 있는 크기로 펴 준다. 지수함수는 그 반대다. LLM에서 확률을 다루는 거의 모든 코드가 로그 공간에서 동작하는 이유가 여기 있다.

두 함수의 관계

exp(x)ex제곱이고, log(x)는 그 역함수다. 머신러닝 문헌에서 log라고 쓰면 관례적으로 자연로그를 뜻한다. 밑이 2인 로그나 상용로그를 쓸 때는 명시한다.

기억해 둘 성질은 세 개다.

log(a×b)=loga+logb — 곱셈이 덧셈이 된다. log(1)=0, log(0)= — 확률 1은 0으로, 확률 0은 음의 무한대로 간다. exp(0)=1, exp(음수)는 0과 1 사이 — 지수함수는 항상 양수다.

왜 로그 공간에서 사는가

앞 세션에서 본 것처럼 문장의 확률은 조건부확률의 곱이다. 토큰이 500개인 문서라면 1보다 작은 수를 500번 곱해야 한다. 각 확률이 평균 0.1이라고 하면 결과는 10의 마이너스 500승이다. 배정밀도 부동소수점이 표현할 수 있는 가장 작은 수가 대략 10의 마이너스 308승이므로, 이 값은 그냥 0이 된다. 계산 자체가 무너지는 것이다.

로그를 취하면 곱셈이 덧셈이 되므로 이 문제가 사라진다. log(0.1)은 약 −2.3이고, 500번 더하면 −1150이다. 아무 문제 없이 표현되는 수다. 그래서 실무에서 확률을 다룰 때는 거의 항상 로그 확률을 쓴다.

이것이 M7에서 생성 결과의 신뢰도를 볼 때 API가 확률이 아니라 logprob을 돌려주는 이유이고, 손실 함수가 로그를 포함하는 이유이며, log_softmax라는 별도의 함수가 존재하는 이유다.

지수함수의 폭발

반대 방향에도 함정이 있다. 지수함수는 무섭게 빨리 자란다. exp(10)은 약 22026, exp(100)은 10의 43승, exp(1000)은 부동소수점 범위를 넘어 inf가 된다.

문제는 softmax가 지수함수를 쓴다는 점이다. 모델이 내놓는 점수는 크기에 제한이 없어서 수백이 되는 경우가 드물지 않고, 그대로 exp를 씌우면 오버플로가 난다. 그래서 반드시 안정화 처리를 해야 하는데, 그것이 다음다음 세션의 log-sum-exp 트릭이다.

로그가 만드는 척도 감각

로그는 곱셈적 차이를 덧셈적 차이로 바꾼다. 확률이 10배 차이 나면 로그로는 2.3 차이다. 이 성질 덕분에 로그 공간에서는 "몇 배 차이"를 "몇 만큼 차이"로 볼 수 있고, 아주 작은 확률들 사이의 차이도 뭉개지지 않고 보인다.

이 감각은 M5의 scaling law에서 다시 쓰인다. 손실이 모델 크기에 대해 멱법칙을 따른다는 관찰은 양쪽에 로그를 취했을 때 직선이 된다는 뜻이고, 그래서 스케일링 그래프는 항상 양쪽 축이 로그다.

백엔드의 언어로

로그 척도는 이미 익숙한 도구다. 응답 시간 분포를 로그 축으로 보면 꼬리가 드러나고, 데이터 크기를 로그로 보면 몇 배 차이인지가 한눈에 들어온다. 여기서의 용법도 같다. 다만 추가로 수치 안정성을 위한 표현 변환이라는 목적이 붙는다는 점이 다르다. 아주 작은 값을 다뤄야 할 때 표현 방식 자체를 바꾸는 것이다.

흔히 틀리는 지점

확인 질문

  1. 확률 0.05의 로그 확률은 대략 얼마인가? (log(0.05)?)
  2. 토큰 300개짜리 문장의 확률을 직접 곱해서 구하려 하면 무슨 일이 생기는가?
  3. logprob이 −0.05인 토큰과 −3.0인 토큰 중 어느 쪽을 모델이 더 확신했는가?
확인

1. 약 −3.0이다. log(0.05)=log(5)log(100)1.614.61=3.0.

2. 1보다 작은 수를 300번 곱하면 부동소수점 표현 범위 아래로 내려가 값이 0이 된다. 언더플로로 정보가 완전히 사라진다.

3. −0.05 쪽이다. 로그 확률은 0에 가까울수록 확률이 1에 가깝다. −0.05는 확률 약 0.95, −3.0은 약 0.05다.


Session 14softmax

softmax는 크기에 제한이 없는 점수들을 합이 1인 확률분포로 바꾸는 변환이다. 지수함수를 씌워 양수로 만든 뒤 전체 합으로 나눈다. 온도라는 손잡이가 바로 이 변환 안으로 들어간다.

문제 설정

모델의 마지막 층은 어휘의 각 항목에 점수를 하나씩 매긴다. 이 점수를 logit이라고 부른다. logit은 그냥 실수라서 음수일 수도 있고 수백이 될 수도 있다. 이것을 확률로 쓰려면 두 조건을 만족시켜야 한다. 모두 0 이상이어야 하고, 전부 더하면 1이어야 한다.

지수함수를 씌우면 첫 번째 조건이 해결된다. exp는 어떤 입력에 대해서도 양수를 내놓기 때문이다. 그다음 전체 합으로 나누면 두 번째 조건도 해결된다. 이것이 softmax다.

softmax(z)i=exp(zi)/Σjexp(zj)

왜 하필 지수함수인가

양수로 만드는 방법은 여러 가지가 있다. 절댓값을 취하거나 제곱해도 된다. 그런데 지수함수를 쓰는 데는 이유가 있다.

첫째, 순서가 보존된다. 점수가 크면 확률도 크다. 절댓값은 이 성질을 깨뜨린다.

둘째, 점수의 차이만이 결과를 결정한다. 모든 점수에 같은 상수를 더해도 softmax 결과가 변하지 않는다. exp(zi+c)/Σexp(zj+c)=exp(c)exp(zi)/exp(c)Σexp(zj)에서 exp(c)가 약분되기 때문이다. 이 성질은 다음 세션의 수치 안정화 트릭의 근거가 된다.

셋째, 미분이 아주 깔끔하다. 특히 cross-entropy와 짝지으면 기울기가 놀랄 만큼 단순해지는데, 이것이 세션 17에서 다룰 내용이다.

온도

softmax에 들어가기 전에 점수를 상수로 나누는 것이 온도 조절이다.

softmax(z/T)

T가 1보다 작으면 점수의 차이가 벌어지고, 지수함수가 그 차이를 증폭해서 1등에 확률이 몰린다. T가 1보다 크면 차이가 줄어들어 분포가 평평해진다. T가 0에 가까워지면 1등에 확률 1이 몰려 사실상 가장 높은 점수를 항상 고르는 것과 같아지고, T가 무한대로 가면 균등분포가 된다.

간단한 숫자로 확인해 보자. 점수가 [2, 1, 0]이라고 하자.

T=1이면 확률은 대략 [0.665, 0.245, 0.090]이다. T=0.5면 점수가 [4, 2, 0]이 되고 확률은 [0.867, 0.117, 0.016]으로 1등이 더 강해진다. T=2면 점수가 [1, 0.5, 0]이 되고 확률은 [0.506, 0.307, 0.186]으로 평평해진다.

여기서 중요한 사실 하나. 온도는 모델의 계산을 바꾸지 않는다. logit은 그대로이고 그것을 확률로 바꾸는 방식만 바뀐다. 즉 온도는 학습된 지식이나 판단을 바꾸는 것이 아니라 선택의 과감함을 조절하는 장치다.

포화라는 위험

softmax의 지수함수는 입력 차이에 민감하다. 점수 차이가 10이면 exp(10)22026배 차이가 나서 사실상 1등이 전부 가져간다. 이 상태를 포화라고 부르는데, 두 가지 문제를 만든다.

생성 측면에서는 다양성이 사라진다. 여러 후보를 섞을 여지가 없어진다.

학습 측면에서는 더 심각하다. 포화된 지점에서 softmax의 기울기는 거의 0이다. 출력이 이미 0과 1에 붙어 있어서 입력을 조금 바꿔도 출력이 거의 변하지 않기 때문이다. 기울기가 0이면 학습 신호가 전달되지 않는다. 세션 12에서 본 d 나누기가 정확히 이 포화를 막기 위한 조치다.

백엔드의 언어로

softmax는 여러 후보의 점수를 확률적 라우팅 비율로 바꾸는 정규화기다. 가중치 기반 로드밸런서에서 각 서버의 가중치를 비율로 환산하는 것과 형태가 같다. 온도는 그 비율을 얼마나 극단적으로 만들지 조절하는 파라미터에 해당한다.

한 가지 다른 점은 여기서 변환이 지수적이라는 것이다. 선형 정규화라면 점수 차이가 비율 차이에 그대로 반영되지만, 지수 정규화에서는 작은 점수 차이가 큰 비율 차이로 증폭된다.

확인 질문

  1. 모든 logit에 100을 더하면 softmax 결과는 어떻게 되는가? 왜인가?
  2. 온도를 0.1로 낮추면 출력이 어떻게 변하고, 그것이 왜 그런지 설명해 보라.
  3. softmax가 포화되면 학습에 어떤 문제가 생기는가?
확인

1. 전혀 변하지 않는다. 분자와 분모에 exp(100)이 공통으로 곱해져 약분되기 때문이다. softmax는 점수의 절대값이 아니라 차이에만 의존한다.

2. 점수를 0.1로 나누면 차이가 10배로 벌어지고, 지수함수가 그 차이를 크게 증폭한다. 결과적으로 1등 후보에 확률이 거의 전부 몰려 사실상 결정적인 선택이 된다.

3. 포화된 지점에서는 입력이 조금 변해도 출력이 거의 변하지 않으므로 기울기가 0에 가까워진다. 역전파로 전달할 학습 신호가 사라져 그 부분의 학습이 멈춘다.


Session 15수치 안정성과 log-sum-exp

exp(1000)은 무한대가 된다. softmax를 그대로 구현하면 실제로 이 일이 벌어지고, 해결책은 모든 점수에서 최댓값을 빼는 것이다. 결과는 수학적으로 완전히 동일한데 오버플로만 사라진다.

문제를 눈으로 보기

점수가 [1000, 999, 998]이라고 하자. 사람 눈에는 별 문제 없어 보인다. 차이가 1씩이니 확률도 적당히 나눠 가질 것 같다. 실제 정답은 대략 [0.665, 0.245, 0.090]으로 앞 세션의 [2, 1, 0]과 똑같다. 차이만 같으면 결과가 같기 때문이다.

그런데 정의대로 계산하면 exp(1000)을 구해야 한다. 배정밀도 부동소수점의 최대값이 약 1.8×10308인데 exp(1000)은 대략 10^434다. 결과는 inf다. 그리고 inf / infNaN이다. 확률이 전부 NaN이 되고, 손실도 NaN이 되고, 역전파로 NaN이 퍼져 모든 파라미터가 오염된다. 한 번의 오버플로가 학습 전체를 죽인다.

반대 방향도 있다. 점수가 [-1000, -1001]이면 exp(1000)이 0으로 언더플로되고, 분모가 0이 되어 다시 NaN이다.

최댓값 빼기

해결책은 앞 세션에서 본 성질을 그대로 쓰는 것이다. 모든 점수에 같은 상수를 더하거나 빼도 softmax 결과는 변하지 않는다. 그러니 가장 큰 점수를 빼 버리면 된다.

softmax(z)i=exp(zimax(z))/Σjexp(zjmax(z))

이렇게 하면 지수함수의 입력 중 최댓값이 정확히 0이 되고, exp(0)=1이다. 나머지는 전부 음수이므로 exp의 결과가 0과 1 사이에 놓인다. 오버플로가 원천적으로 불가능해진다.

[1000, 999, 998]에 적용하면 [0, -1, -2]가 되고, exp를 씌우면 [1, 0.368, 0.135], 합인 1.503으로 나누면 [0.665, 0.245, 0.090]이다. 정확히 원하는 값이 나온다.

언더플로 쪽은 어떨까. 아주 작은 점수의 exp가 0이 되는 것은 여전하지만, 최댓값에 해당하는 항이 반드시 1이므로 분모가 0이 되는 일은 결코 없다. 작은 항이 0으로 뭉개지는 것은 어차피 그 확률이 무시할 만하다는 뜻이라 실질적인 손해가 없다.

log-sum-exp

같은 아이디어가 로그 확률을 계산할 때도 쓰인다. logΣexp(zj)라는 양이 자주 필요한데, 이것을 안정적으로 계산하는 항등식이 이렇다.

logΣjexp(zj)=max(z)+logΣjexp(zjmax(z))

이것을 log-sum-exp 트릭이라고 부른다. 이 형태를 쓰면 log_softmax를 한 번에 안정적으로 계산할 수 있고, 그래서 프레임워크들이 softmaxlog를 취하는 대신 log_softmax라는 별도 함수를 제공한다. 두 번 계산하면 중간에 정밀도가 손실되지만, 합쳐서 계산하면 그렇지 않다.

이 아이디어가 다시 나오는 곳

최댓값을 추적하면서 지수합을 갱신한다는 발상은 여기서 끝나지 않는다. M8의 FlashAttention이 정확히 이 아이디어의 확장이다. 거대한 attention 점수 행렬을 통째로 메모리에 올리지 않고 조각조각 처리하면서, 지금까지 본 최댓값과 부분 합을 들고 다니며 결과를 보정해 나간다. 이것을 online softmax라고 부르는데, 그 재귀식의 뿌리가 이 세션의 항등식이다.

백엔드의 언어로

이것은 표현 범위를 고려한 계산 순서 재배치다. 수학적으로 동등한 여러 계산 경로 중에서 중간값이 표현 범위를 벗어나지 않는 경로를 고르는 것이다. 큰 수의 나눗셈을 먼저 하고 곱셈을 나중에 하는 식의 정수 연산 관용구와 성격이 같다.

실무적으로 더 중요한 교훈은 이것이다. 수치 문제는 조용히 시작해서 크게 터진다. 오버플로 한 번이 NaN이 되고, NaN은 전파되며, 손실이 NaN이 되고 나면 그때는 이미 원인 지점에서 한참 떨어져 있다. 그래서 M2에서 학습 곡선을 읽을 때 NaN 추적 절차를 따로 다룬다.

확인 질문

  1. 점수 [1000, 999, 998][2, 1, 0]의 softmax 결과가 같은 이유는?
  2. 최댓값을 빼면 왜 오버플로가 원천적으로 불가능해지는가?
  3. 최댓값을 빼도 언더플로는 남는데, 왜 그것은 문제가 되지 않는가?
확인

1. softmax는 점수의 차이에만 의존하기 때문이다. 두 벡터의 차이 구조가 동일하므로(1씩 감소) 결과도 동일하다.

2. 최댓값을 빼면 지수함수의 입력 중 가장 큰 값이 정확히 0이 되고 나머지는 전부 음수가 된다. exp의 결과가 모두 0과 1 사이에 놓이므로 표현 범위를 넘을 수 없다.

3. 최댓값에 해당하는 항이 항상 1이므로 분모가 0이 되지 않는다. 언더플로로 0이 되는 항은 원래 확률이 무시할 만큼 작았던 항이므로 결과에 실질적인 영향이 없다.


Session 16entropy와 surprisal

어떤 결과가 확률 p로 일어났을 때의 놀라움은 logp다. 엔트로피는 그 놀라움의 평균이고, 분포가 얼마나 퍼져 있는지를 재는 하나의 숫자다.

놀라움을 숫자로

정보량이라는 개념을 만들려면 먼저 원하는 성질을 정해야 한다. 자주 일어나는 일을 관측했을 때는 정보를 적게 얻고, 드문 일을 관측했을 때는 많이 얻어야 한다. 그리고 서로 독립인 두 사건을 함께 관측했을 때 얻는 정보는 각각의 합이어야 한다.

이 두 조건을 만족하는 함수는 사실상 하나뿐이다. logp다. 확률이 1이면 log1=0으로 아무 정보도 얻지 못하고, 확률이 작아질수록 값이 커진다. 그리고 독립 사건의 확률은 곱해지는데 로그가 곱셈을 덧셈으로 바꿔 주므로 두 번째 조건도 만족한다. 이 값을 surprisal, 우리말로는 자기정보라고 부른다.

숫자로 보자. 확률 0.5인 사건의 surprisal은 log(0.5)0.69다. 확률 0.01이면 log(0.01)4.6이다. 확률이 50배 낮아지니 놀라움은 대략 6.7배 커진다. 로그 척도라 배수가 아니라 덧셈으로 커지는 것이 특징이다.

로그의 밑을 2로 잡으면 단위가 비트가 되고, 자연로그를 쓰면 nats라고 부른다. 머신러닝에서는 자연로그가 기본이므로 손실 값의 단위는 nats다. 비트로 보고 싶으면 log20.693으로 나누면 된다.

엔트로피는 평균 놀라움

엔트로피는 분포 p에서 뽑을 때 기대되는 surprisal이다.

H(p)=Σipilogpi

각 결과의 놀라움 logpi에 그 확률 pi를 곱해서 더한 것, 즉 기댓값이다. 세션 12에서 배운 기댓값이 여기서 쓰인다.

이 값이 무엇을 재는지는 극단을 보면 분명하다. 한 결과의 확률이 1이고 나머지가 0이면 엔트로피는 0이다. 결과가 이미 정해져 있으니 관측해도 얻을 정보가 없다. 반대로 n개의 결과가 모두 같은 확률 1/n이라면 엔트로피는 logn으로 최대가 된다. 가장 예측하기 어려운 상태다.

그래서 엔트로피는 분포가 얼마나 평평한가, 곧 모델이 얼마나 확신하지 못하는가를 재는 숫자로 읽으면 된다.

유효 후보 수

엔트로피를 직관적으로 쓰는 방법 하나를 소개한다. exp(H)를 계산하면 "실질적으로 몇 개의 후보가 경쟁하고 있는가"에 해당하는 수가 나온다. 균등분포에서 후보가 n개면 H=logn이고 exp(H)=n이므로 정확히 후보 수가 나온다. 분포가 치우쳐 있으면 그보다 작은 수가 나오고, 그 값이 "체감 후보 수"에 해당한다.

모델이 어떤 위치에서 exp(H)=1.2를 보인다면 사실상 답이 하나로 정해진 자리다. exp(H)=40이라면 마흔 개쯤이 실질적으로 경쟁하는 열린 자리다. 문장 안에서 이 값이 위치마다 크게 다르다는 사실은 생성 품질을 이해하는 데 도움이 된다. 조사 자리에서는 낮고, 새로운 명제가 시작되는 자리에서는 높다.

백엔드의 언어로

엔트로피는 분포의 불확실성을 하나의 스칼라로 요약한 지표다. 로그를 씌운 유효 카디널리티라고 봐도 크게 어긋나지 않는다. 어떤 컬럼의 값이 한 종류밖에 없으면 엔트로피가 0이고, 값이 고르게 흩어져 있으면 높다. 인덱스 선택도를 판단할 때 쓰는 감각과 같은 종류의 양이다.

압축 쪽 대응도 정확하다. 엔트로피는 그 분포를 따르는 데이터를 손실 없이 압축했을 때 필요한 평균 부호 길이의 이론적 하한이다. 이 사실이 다음 세션에서 cross-entropy가 왜 손실 함수가 되는지를 설명한다.

확인 질문

  1. 확률이 [1.0, 0, 0]인 분포와 [1/3, 1/3, 1/3]인 분포의 엔트로피를 각각 구하고 의미를 말해 보라.
  2. surprisal을 logp로 정의한 두 가지 이유를 말해 보라.
  3. 어떤 위치에서 exp(H)=2.1이 나왔다면 이것을 어떻게 해석하는가?
확인

1. 앞은 0이다. 결과가 정해져 있어 얻을 정보가 없다. 뒤는 log31.10 nats로 세 결과 중 최대다. 가장 예측하기 어려운 상태다.

2. 첫째, 확률이 작을수록 값이 커져 "드문 일일수록 정보가 많다"는 요구를 만족한다. 둘째, 독립 사건의 확률은 곱해지는데 로그가 이를 덧셈으로 바꿔 주므로 정보량이 더해진다는 요구도 만족한다.

3. 그 자리에서 실질적으로 두 개 남짓의 후보가 경쟁하고 있다는 뜻이다. 완전히 결정된 자리도 아니고 열린 자리도 아닌 중간 상태다.


Session 17cross-entropy

cross-entropy는 정답 분포 대신 모델의 분포를 믿고 부호를 배정했을 때 지불하는 평균 비용이다. 정답이 한 토큰으로 정해져 있는 언어 모델에서는 이 값이 log(정답토큰에모델이확률)로 간단해지고, 그것이 곧 학습 손실이다.

정의

정답 분포 p와 모델 분포 q가 있을 때 cross-entropy는 이렇게 정의된다.

H(p,q)=Σipilogqi

엔트로피 H(p)=Σpilogpi와 비교하면 차이가 한 곳뿐이다. 로그 안이 pi에서 qi로 바뀌었다. 즉 놀라움은 모델의 분포로 계산하되, 평균은 실제 분포로 낸다.

압축의 언어로 읽으면 뜻이 분명해진다. 부호 길이는 logqi로 모델의 예측에 따라 배정하는데, 실제로 각 기호가 등장하는 빈도는 pi다. 그래서 평균 부호 길이가 Σpilogqi가 된다. 모델의 예측이 실제와 어긋날수록 이 값이 커진다.

언어 모델에서 단순해지는 이유

학습할 때 정답은 특정 토큰 하나다. 그러면 p는 그 토큰에만 1이고 나머지는 전부 0인 one-hot 분포다. 이것을 위 식에 넣으면 합의 항 중 정답 토큰에 해당하는 항만 살아남는다.

H(p,q)=logq정답

모델이 정답 토큰에 배정한 확률의 로그에 마이너스를 붙인 것이 손실이다. 모델이 정답에 확률 0.9를 줬다면 손실은 약 0.105, 0.1을 줬다면 약 2.30, 0.01을 줬다면 약 4.61이다. 정답에 확률을 많이 줄수록 손실이 작다.

이 식을 최대우도추정의 관점으로도 읽을 수 있다. 학습 데이터의 확률을 최대화하는 것이 목표라면, 각 위치의 확률을 곱해서 최대화해야 한다. 로그를 취하면 곱이 합이 되고, 최대화를 최소화로 바꾸려고 부호를 뒤집으면 정확히 위 식의 합이 나온다. cross-entropy 최소화와 최대우도추정은 같은 것이다.

손실 값을 읽는 법

이 관계를 알면 학습 로그의 숫자가 의미를 갖는다.

어휘 크기가 V인 모델을 무작위로 초기화하면 모든 토큰에 균등하게 1/V를 배정하므로 손실은 log(1/V)=logV가 된다. 어휘가 5만이면 약 10.8, 10만이면 약 11.5다. 학습을 시작했을 때 첫 손실이 이 값 근처가 아니면 무언가 잘못된 것이다. 초기화가 이상하거나 정답 정렬이 어긋났을 가능성이 높다. 이것이 M2에서 다시 강조할 가장 값싸고 강력한 점검이다.

학습이 진행되면서 손실이 2.0 근처로 내려간다면, 모델이 정답 토큰에 평균적으로 exp(2.0)0.135 정도의 확률을 주고 있다는 뜻이다. 1.0이면 약 0.37이다. 손실 숫자를 확률로 환산해 보는 습관이 있으면 학습 상태를 훨씬 구체적으로 감각할 수 있다.

softmax와 짝지었을 때의 기울기

한 가지 놀라운 사실이 있다. softmax와 cross-entropy를 붙여 놓으면 logit에 대한 기울기가 극단적으로 단순해진다.

L/z=qp

모델이 낸 확률에서 정답 분포를 뺀 것. 그게 전부다. 정답 토큰에 대해서는 q_정답 - 1이고 나머지는 q_i다. 지수함수도 로그도 전부 사라지고 뺄셈만 남는다.

이 단순함이 두 함수를 항상 붙여 쓰는 진짜 이유다. 계산이 빠르고, 수치적으로 안정적이며, 기울기의 의미도 직관적이다. "정답에 준 확률이 모자란 만큼 그 방향으로 밀어라"는 것이 문자 그대로 식에 적혀 있다. 그래서 프레임워크들이 softmaxcross_entropy를 따로 부르지 말고 합쳐진 함수를 쓰라고 권하는 것이다.

백엔드의 언어로

cross-entropy는 예측 확률에 대한 벌점 함수인데, 벌점의 모양이 특이하다. 확실하게 틀렸을 때 벌점이 무한대로 발산한다. 정답에 확률 0을 줬다면 log0=다. 이 성질 때문에 모델은 "절대 아니다"라고 단정하는 것을 극도로 꺼리게 되고, 결과적으로 모든 토큰에 아주 작은 확률이라도 남겨 두게 된다.

이것은 시스템 설계에서 자주 보는 비대칭 벌점과 성격이 같다. 어떤 종류의 실수는 다른 실수보다 훨씬 비싸게 매기는 구조 말이다.

확인 질문

  1. 어휘 크기 32000인 모델의 학습 첫 손실은 대략 얼마여야 하는가?
  2. 손실이 1.6이라면 모델은 정답 토큰에 평균 어느 정도의 확률을 주고 있는가?
  3. softmax와 cross-entropy를 붙여 쓸 때 logit에 대한 기울기는 무엇인가? 이 식을 말로 풀면?
확인

1. log(32000)10.37이다. 이 값에서 크게 벗어나면 초기화나 라벨 정렬을 의심해야 한다.

2. exp(1.6)0.20, 즉 약 20%다.

3. qp, 즉 모델의 예측 확률에서 정답 분포를 뺀 것이다. "정답에 준 확률이 1에서 모자란 만큼 정답 쪽으로 밀고, 오답에 준 확률만큼 그쪽에서 빼라"는 뜻이다.

앉아서 읽을 것

시간이 날 때 Goodfellow의 Deep Learning 3장 중 정보이론 절(3.13)을 읽으면 이 세션과 다음 세션이 한 흐름으로 정리된다. 분량은 두 쪽 남짓이다.


Session 18KL divergence

KL divergence는 모델 분포가 정답 분포에서 얼마나 벗어났는가를 재는 양이고, H(p,q) - H(p), 즉 cross-entropy에서 어쩔 수 없는 부분을 뺀 나머지다. 사후학습에서 모델이 원래 모습에서 너무 멀어지지 않게 붙잡는 밧줄로 쓰인다.

정의와 분해

KL(pq)=Σipilog(pi/qi)

로그 안의 나눗셈을 뺄셈으로 풀면 이렇게 된다.

KL(pq)=Σpilogqi(Σpilogpi)=H(p,q)H(p)

cross-entropy에서 엔트로피를 뺀 것이다. 이 분해가 의미하는 바가 중요하다. H(p)는 정답 분포 자체가 갖는 불확실성으로, 모델이 아무리 완벽해도 없앨 수 없는 부분이다. KL은 그 위에 모델의 부정확함 때문에 추가로 지불하는 비용이다.

여기서 앞 세션의 한 가지가 설명된다. 언어 모델 학습에서 정답이 one-hot이면 H(p)=0이므로 KL(pq)=H(p,q)가 된다. cross-entropy를 최소화하는 것과 KL을 최소화하는 것이 정확히 같은 일인 것이다. 그래서 두 이름이 같은 맥락에서 섞여 쓰인다.

성질도 두 가지만 알면 된다. KL은 항상 0 이상이고, 두 분포가 같을 때만 0이다. 그리고 대칭이 아니다. KL(pq)KL(qp)는 다른 값이고, 어느 쪽을 쓰느냐가 실제로 다른 동작을 만든다.

비대칭이 만드는 차이

이 비대칭은 실무적으로 의미가 있다. KL(pq)를 최소화하면 p가 큰 곳에서 q도 커야 한다. p가 큰데 q가 0이면 로그가 발산해 벌점이 무한대가 되기 때문이다. 그래서 결과적으로 qp의 모든 봉우리를 덮으려고 퍼진다. 반대로 KL(qp)를 최소화하면 qp의 봉우리 하나에 집중해도 벌점이 없다. 하나만 잘 맞히면 되는 것이다.

이 차이는 생성 모델의 성격을 가른다. 전자는 다양성을 확보하지만 흐릿해지고, 후자는 선명하지만 모드를 놓친다. 지금 단계에서 외울 필요는 없지만, 나중에 "KL의 방향"이라는 말을 만났을 때 그것이 진짜로 결과를 바꾸는 선택이라는 점만 기억해 두면 된다.

사후학습에서의 역할

KL이 이 트랙에서 다시 크게 등장하는 곳은 M6이다. 선호 최적화에서 우리는 모델이 사람이 선호하는 방향으로 움직이기를 원하지만, 너무 멀리 가면 문제가 생긴다. 보상 점수만 좇다가 언어 능력 자체를 잃어버리는 현상, 이른바 reward hacking이다. 극단적인 경우 모델이 의미 없는 문자열을 반복하는데 보상 모델은 그것을 높게 평가하는 상황까지 간다.

그래서 목적 함수에 항을 하나 더한다. "보상을 높이되, 원래 모델에서 KL 거리가 멀어지는 만큼 벌점을 물어라." 이 밧줄의 세기를 조절하는 계수가 흔히 beta라고 불리는 값이고, DPO에서도 같은 역할로 등장한다. 이 계수를 키우면 안전하지만 변화가 적고, 줄이면 크게 변하지만 무너질 위험이 커진다. M6에서 이 트레이드오프를 직접 다룬다.

백엔드의 언어로

KL은 기준 버전 대비 변화량의 크기를 재는 지표다. 새 버전이 기존 동작에서 얼마나 벗어났는지를 하나의 숫자로 요약하는 것이다. 그리고 KL 제약을 건다는 것은 "성능을 개선하되 기존 동작에서 너무 벗어나지 마라"는 제약을 최적화 목표 안에 직접 써 넣는 일이다. 배포 시 카나리 비율을 제한하거나 변경 폭에 상한을 두는 것과 같은 발상인데, 여기서는 그것이 수식 안에 들어가 있다.

확인 질문

  1. KL(pq)=H(p,q)H(p)라는 분해에서 각 항이 무엇을 뜻하는지 말해 보라.
  2. 언어 모델 학습에서 cross-entropy 최소화와 KL 최소화가 같아지는 이유는?
  3. 사후학습에서 KL 항을 빼면 어떤 일이 생기는가?
확인

1. H(p)는 정답 분포 자체의 불확실성으로 모델이 완벽해도 없앨 수 없는 부분이고, KL은 모델이 부정확해서 추가로 지불하는 비용이다. 둘의 합이 실제 지불하는 총비용인 cross-entropy다.

2. 정답이 특정 토큰 하나인 one-hot 분포라서 H(p)=0이기 때문이다. 뺄 것이 없으므로 두 양이 같아진다.

3. 모델이 보상 점수만 좇아 원래 분포에서 무한정 멀어진다. 보상 모델의 허점을 파고드는 출력을 내면서 언어 능력 자체가 무너지는 reward hacking이 일어난다.


Session 19perplexity

perplexity는 exp(crossentropy)이고, 모델이 매 위치에서 실질적으로 몇 개의 후보를 두고 헷갈리는가로 읽으면 된다. 손실 값을 사람이 감각할 수 있는 척도로 옮긴 것이다.

정의와 직관

PPL=exp(H)=exp(crossentropy)

세션 16에서 exp(엔트로피)를 유효 후보 수로 읽었던 것과 같은 발상이다. 손실이 2.3이면 perplexity는 약 10이고, "평균적으로 열 개쯤의 후보를 두고 고민하고 있다"로 읽는다. 손실이 1.6이면 약 5, 손실이 4.6이면 약 100이다.

균등분포를 가정하면 이 해석이 정확히 맞는다. 어휘 V개에 균등하게 확률을 주는 모델의 손실은 logV이고 perplexity는 정확히 V다. 실제 모델은 확률을 고르게 주지 않으므로 perplexity가 어휘 크기보다 훨씬 작다.

왜 손실 대신 perplexity를 보는가

두 척도는 단조 대응이므로 정보량은 같다. 그런데도 perplexity를 쓰는 이유는 사람의 감각에 걸리기 때문이다. 손실이 2.4에서 2.2로 줄었다는 것보다, perplexity가 11.0에서 9.0으로 줄었다는 것이 개선의 크기를 체감하기 쉽다.

동시에 함정도 있다. perplexity는 손실의 지수함수이므로 작은 손실 변화가 큰 perplexity 변화로 보인다. 손실 0.1의 차이가 perplexity로는 10% 넘는 차이로 나타난다. 개선이 실제보다 크게 보일 수 있으니, 비교할 때는 어느 척도로 이야기하는지 명시하는 편이 좋다.

비교할 때의 함정

perplexity로 두 모델을 비교할 때 반드시 확인해야 할 것이 있다. 토크나이저가 같아야 한다.

이유는 이렇다. perplexity는 토큰당 값이다. 토크나이저 A가 어떤 문장을 100토큰으로 자르고 B가 같은 문장을 150토큰으로 자른다면, B는 토큰 하나하나가 더 잘게 쪼개져 있어서 예측하기 쉽다. 같은 실력의 모델이라도 B 쪽 perplexity가 낮게 나온다. 모델이 더 나아서가 아니라 문제가 더 쉬워진 것이다.

이 문제 때문에 서로 다른 토크나이저를 쓰는 모델을 비교할 때는 바이트당 비트수 같은 토크나이저 독립적인 지표를 쓰거나, 아예 다운스트림 과제 성능으로 비교한다. 한국어 모델을 비교할 때 특히 중요한데, 한국어 토크나이저의 효율 차이가 크기 때문이다. 이 이야기는 M3에서 다시 만난다.

분포의 뾰족함 · 엔트로피 · perplexity

0.35
손잡이를 왼쪽으로 밀면 분포가 균등해지고, 오른쪽으로 밀면 한 후보에 몰린다. 엔트로피와 perplexity가 함께 움직이는 것을 보라. 이 셋은 같은 사실을 세 가지 척도로 말한 것이다. 학습이란 결국 정답 토큰 쪽으로 이 분포를 뾰족하게 만드는 과정이다.

백엔드의 언어로

perplexity는 원지표를 사람이 읽기 쉬운 단위로 환산한 파생 지표다. 로그 스케일로 저장된 값을 화면에는 선형 단위로 보여 주는 것과 같다. 원본과 정보량이 같으니 어느 쪽으로 계산하든 상관없지만, 소통할 때는 환산된 쪽이 유용하다.

한 가지 주의는 앞서 말한 대로 비교 조건이다. 서로 다른 전처리를 거친 시스템의 지표를 나란히 놓으면 안 된다는 원칙은 여기서도 그대로 적용된다.

확인 질문

  1. cross-entropy 손실 3.0인 모델의 perplexity는?
  2. 토크나이저가 다른 두 모델의 perplexity를 직접 비교하면 안 되는 이유는?
  3. 어휘 5만 개에 균등한 확률을 주는 모델의 perplexity는 얼마인가?
확인

1. exp(3.0)20.1이다. 평균적으로 스무 개쯤의 후보를 두고 고민하는 상태다.

2. perplexity는 토큰당 값이기 때문이다. 같은 문장을 더 잘게 쪼개는 토크나이저를 쓰면 토큰 하나하나가 예측하기 쉬워져 perplexity가 낮게 나온다. 모델의 실력 차이가 아니라 문제 난이도의 차이다.

3. 정확히 50000이다. 균등분포의 손실은 logV이고 exp(logV)=V이기 때문이다.


Session 20gradient와 chain rule

gradient는 파라미터를 어느 방향으로 밀면 손실이 가장 빨리 줄어드는지를 알려 주는 벡터다. chain rule은 그 정보를 출력에서 입력 쪽으로 거꾸로 전달하는 규칙이고, 신경망 학습은 이 두 가지가 전부다.

기울기와 방향

일변수 함수에서 미분은 접선의 기울기다. f'(x)가 양수면 x를 늘릴 때 f가 커지고, 음수면 작아진다. 그러니 f를 줄이고 싶으면 기울기의 반대 방향으로 움직이면 된다.

변수가 여럿이면 각 변수에 대한 편미분을 모아 벡터로 만든다. 이것이 gradient이고 f로 쓴다. 편미분 f/x1은 "다른 변수는 고정하고 x1만 조금 움직였을 때 f가 얼마나 변하는가"다.

gradient에는 중요한 기하학적 성질이 있다. gradient 방향이 함수가 가장 가파르게 증가하는 방향이다. 그러니 손실을 줄이려면 그 반대 방향으로 가면 된다. 이것이 경사하강법이다.

θθηL(θ)

θ는 파라미터, η는 학습률이다. 학습률은 한 걸음의 크기를 정한다. 너무 크면 최솟값을 지나쳐 튕겨 나가고, 너무 작으면 하염없이 느리다. M2에서 이 값을 다루는 방법을 자세히 본다.

chain rule

문제는 신경망에서 손실이 파라미터의 함수로 직접 쓰이지 않는다는 점이다. 파라미터가 첫 층의 출력을 만들고, 그것이 둘째 층의 입력이 되고, 그렇게 수십 층을 거쳐 마지막에 손실이 나온다. 중간에 낀 파라미터가 손실에 미치는 영향을 어떻게 계산하나.

chain rule이 답이다. 합성함수의 미분은 각 단계 미분의 곱이다.

dL/dx=(dL/dy)×(dy/dx)

말로 풀면 이렇다. xy를 통해 L에 영향을 준다면, xL에 미치는 영향은 "xy에 미치는 영향"과 "yL에 미치는 영향"의 곱이다.

층이 여러 개면 이 곱이 층 수만큼 이어진다. 그리고 곱은 뒤에서부터 계산하는 것이 훨씬 싸다. 이유는 이렇다. 신경망의 출력인 손실은 스칼라 하나이고 입력인 파라미터는 수십억 개다. 앞에서부터 계산하면 파라미터 하나하나에 대해 전체 경로를 따라가야 하지만, 뒤에서부터 계산하면 한 번의 역방향 통과로 모든 파라미터의 gradient를 얻는다. 이것이 역전파이고, 다음 모듈에서 자세히 다룬다.

곱이 만드는 두 가지 사고

chain rule이 곱셈이라는 사실에서 신경망 학습의 고질적인 문제 두 가지가 곧바로 나온다.

각 층의 미분값이 평균적으로 1보다 작으면, 층을 거칠 때마다 곱해지면서 기하급수적으로 작아진다. 50층을 지나면 0.9의 50제곱, 즉 0.005 수준이 된다. 앞쪽 층에 도달하는 학습 신호가 사실상 사라지는 것이다. 이것이 기울기 소실이다.

반대로 1보다 크면 폭발한다. 1.1의 50제곱은 117이다. 값이 급격히 커지면서 NaN으로 이어진다.

이 문제를 완화하는 것이 잔차 연결과 정규화다. 잔차 연결은 gradient가 층을 우회해서 직접 흐를 수 있는 경로를 만들어 곱의 사슬을 끊고, 정규화는 각 층의 출력 크기를 일정하게 유지해 곱의 배율이 1 근처에 머물게 한다. M2에서 이 둘을 다룰 때, 그것이 왜 장식이 아니라 필수인지가 여기서 설명된다.

Jacobian을 만들지 않는 이유

가끔 Jacobian이라는 말이 나온다. 벡터 입력에서 벡터 출력으로 가는 함수의 모든 편미분을 행렬로 모은 것이다. 입력이 n차원, 출력이 m차원이면 [m, n] 행렬이다.

실무에서 이 행렬을 실제로 만드는 일은 거의 없다. 크기가 감당이 안 되기 때문이다. 대신 필요한 것은 어떤 벡터와 Jacobian의 곱뿐인데, 이것은 행렬을 만들지 않고도 계산할 수 있다. 이 연산을 vector-Jacobian product, 줄여서 VJP라고 부르고, 자동미분 프레임워크가 실제로 하는 일이 이것이다. 그래서 "Jacobian이 무엇인지는 알되, 그것을 만들지는 않는다"가 정확한 이해다.

백엔드의 언어로

chain rule은 의존성 그래프를 거슬러 올라가며 책임을 배분하는 절차다. 최종 지표가 나빠졌을 때 각 구간이 얼마나 기여했는지를 역방향으로 계산해 나가는 것과 형태가 같다. 각 구간의 기여도를 곱해서 전체 기여도를 얻고, 그것을 한 번의 역방향 순회로 전부 구한다.

기울기 소실과 폭발은 다단계 파이프라인에서 배율이 누적되는 문제다. 각 단계의 배율이 1에서 조금만 벗어나도 단계가 많아지면 결과가 극단으로 간다. 잔차 연결은 중간을 건너뛰는 우회로를 놓아 이 누적을 끊는 장치다.

확인 질문

  1. gradient의 반대 방향으로 파라미터를 움직이는 이유는?
  2. chain rule을 뒤에서부터 계산하는 것이 앞에서부터 계산하는 것보다 싼 이유를 설명해 보라.
  3. 각 층의 미분값이 0.8일 때 40층을 지나면 학습 신호가 어느 정도로 줄어드는가? 이 현상의 이름은?
확인

1. gradient는 함수가 가장 가파르게 증가하는 방향이므로, 손실을 줄이려면 그 반대 방향으로 가야 한다.

2. 출력인 손실은 스칼라 하나이고 입력인 파라미터는 수십억 개다. 앞에서부터 가면 파라미터마다 전체 경로를 계산해야 하지만, 뒤에서부터 가면 한 번의 역방향 통과로 모든 파라미터의 gradient를 한꺼번에 얻는다.

3. 0.8400.00013으로 약 만분의 일 수준이 된다. 기울기 소실이라고 부른다.


Session 21복습 — 이 모듈의 여섯 기호

이 세션은 새 내용을 배우지 않는다. M1에서 만난 것들을 하나의 사슬로 꿰고, 다음 모듈로 넘어가도 되는지를 스스로 확인한다. 이 트랙의 첫 관문이다.

하나의 사슬

M1에서 배운 것들은 따로 떨어진 열다섯 개의 항목이 아니다. 하나로 이어진 사슬이고, 그 사슬을 소리 내어 말할 수 있으면 이 모듈은 끝난 것이다. 한번 이어 보자.

모델의 마지막 층은 어휘마다 점수를 하나씩 내놓는다. 이 점수는 크기에 제한이 없으므로 확률로 쓸 수 없다. softmax가 지수함수를 씌워 양수로 만들고 전체 합으로 나눠 확률분포로 바꾼다. 이때 지수함수가 폭주할 수 있으므로 반드시 최댓값을 빼서 안정화한다. softmax는 점수의 차이에만 의존하므로 이 조작은 결과를 바꾸지 않는다.

이렇게 얻은 분포가 얼마나 퍼져 있는지를 재는 것이 엔트로피다. 각 결과의 놀라움 logp를 확률로 가중 평균한 값이다. 정답 분포 대신 모델 분포로 놀라움을 계산하면 cross-entropy가 되고, 정답이 토큰 하나로 정해진 언어 모델에서는 이것이 log(정답에확률)로 간단해진다. 이것이 손실이다. cross-entropy에서 어쩔 수 없는 부분인 엔트로피를 빼면 KL divergence, 즉 모델이 정답에서 벗어난 정도만 남는다. 손실을 사람이 읽기 쉽게 지수로 되돌린 것이 perplexity다.

이 손실을 줄이려면 파라미터를 어느 쪽으로 밀어야 하는지 알아야 한다. gradient가 그 방향이고, 층을 거슬러 전달하는 규칙이 chain rule이다. 곱이 이어지므로 배율이 1에서 벗어나면 소실이나 폭발이 생기고, 그래서 잔차 연결과 정규화가 필요해진다.

그리고 이 모든 계산의 바닥에는 내적이 있다. 벡터를 성분끼리 곱해 더하는 연산 하나가 유사도가 되고, 행렬곱이 되고, attention이 되고, 신경망의 모든 층이 된다. 정규화는 그 내적에서 크기의 영향을 빼서 방향만 남기고, 랭크는 그 행렬이 실제로 몇 개의 방향만 쓰고 있는지를 말해 준다.

관문 G1

아래 여덟 개에 막힘없이 소리 내어 답할 수 있어야 M2로 넘어간다. 답이 떠오르지 않는 항목이 있으면 해당 세션으로 돌아간다. 이 관문에서 시간을 쓰는 것이 나중에 M4에서 헤매는 것보다 훨씬 싸다.

  1. softmax는 무엇을 무엇으로 바꾸며, 왜 하필 지수함수를 쓰는가? (S14)
  2. softmax를 그대로 구현하면 왜 터지며, 어떻게 고치는가? 고쳐도 결과가 같은 이유는? (S15)
  3. cross-entropy가 언어 모델에서 log(정답확률)로 단순해지는 이유는? (S17)
  4. 어휘 크기 V인 모델의 학습 첫 손실이 logV여야 하는 이유는? (S17)
  5. cross-entropy · 엔트로피 · KL divergence 셋의 관계를 한 줄짜리 식으로 쓰고 각 항을 해석해 보라. (S18)
  6. perplexity는 무엇의 지수이며, 왜 토크나이저가 다르면 비교하면 안 되는가? (S19)
  7. attention에서 d로 나누는 이유를 분산의 언어로 설명해 보라. (S12)
  8. chain rule이 곱셈이라는 사실에서 어떤 두 가지 문제가 따라 나오는가? (S20)
확인

1. 크기에 제한이 없는 logit을 합이 1인 확률분포로 바꾼다. 지수함수를 쓰는 이유는 세 가지다. 순서가 보존되고, 결과가 점수의 차이에만 의존하며, cross-entropy와 짝지었을 때 기울기가 qp로 극도로 단순해진다.

2. 점수가 크면 exp가 표현 범위를 넘어 inf가 되고 inf/inf=NaN이 된다. 모든 점수에서 최댓값을 빼면 지수 입력의 최댓값이 0이 되어 오버플로가 불가능해진다. softmax는 점수의 차이에만 의존하므로 결과는 동일하다.

3. 정답이 특정 토큰 하나인 one-hot 분포라서 합의 항 중 정답 항만 살아남기 때문이다.

4. 무작위 초기화 상태에서는 모든 토큰에 균등하게 1/V를 배정하므로 손실이 log(1/V)=logV가 된다. 이 값에서 크게 벗어나면 초기화나 라벨 정렬에 문제가 있다.

5. KL(pq)=H(p,q)H(p). H(p)는 정답 분포 고유의 불확실성으로 없앨 수 없는 부분, KL은 모델의 부정확함 때문에 추가로 지불하는 비용, H(p,q)는 실제 지불 총액이다.

6. cross-entropy의 지수다. 토큰당 값이므로, 같은 문장을 더 잘게 자르는 토크나이저를 쓰면 예측이 쉬워져 값이 낮게 나온다. 모델 실력이 아니라 문제 난이도의 차이다.

7. 각 항의 분산이 1인 항 d개를 더하면 합의 분산이 d가 되고 표준편차가 d가 된다. 이 값이 그대로 softmax에 들어가면 분포가 포화되어 기울기가 사라지므로, d로 나눠 분산을 1로 되돌린다.

8. 층마다의 미분값이 곱해지므로, 평균 배율이 1보다 작으면 기하급수적으로 줄어드는 기울기 소실이, 1보다 크면 폭발이 일어난다.

모듈을 마치며

이 모듈에서 익힌 것은 계산 능력이 아니라 읽는 능력이다. 앞으로 논문이나 문서에서 softmax(QKT/d)V 같은 식을 만났을 때, 그것이 그림이 아니라 문장으로 읽혀야 한다. 지금은 아직 QK가 무엇인지 모르지만, d로 나누는 이유와 softmax가 하는 일은 이미 안다. M4에서 나머지 빈칸이 채워진다.

다음 모듈은 신경망이다. 여기서 배운 gradient와 chain rule이 실제로 어떻게 층을 타고 흐르는지, 그리고 학습 한 스텝에서 정확히 무슨 일이 벌어지는지를 열두 번의 이동에 걸쳐 본다.

M2세션 22–3413개

신경망과 학습의 원리

층 하나가 하는 일에서 시작해 학습 한 스텝에서 벌어지는 모든 일까지 따라간다. 여기서 익히는 잔차 연결과 정규화는 M4의 Transformer 블록에 그대로 들어간다.

Session 22뉴런에서 선형 계층까지

신경망의 기본 단위는 y=Wx+b, 즉 가중치를 곱하고 편향을 더하는 것이다. 이름은 거창하지만 M1에서 배운 행렬곱에 상수 벡터 하나를 더한 것이 전부다.

뉴런 하나

가장 작은 단위부터 보자. 입력 벡터 x를 받아 숫자 하나를 내놓는 장치를 생각한다. 계산은 이렇다. 입력의 각 성분에 가중치를 곱해서 전부 더하고, 마지막에 상수를 하나 더한다.

out=w1x1+w2x2++wnxn+b

앞부분은 M1 세션 6에서 배운 내적이다. 즉 뉴런 하나는 입력과 자기 가중치 벡터의 내적에 편향을 더한 것이다. 내적이 방향의 일치도를 재는 연산이었으니, 뉴런은 "입력이 내가 찾는 패턴과 얼마나 닮았는가"를 점수로 내놓는 장치라고 읽을 수 있다.

편향 b의 역할은 기준점 이동이다. 편향이 없으면 입력이 0일 때 출력도 반드시 0이 되는데, 이 제약이 표현력을 불필요하게 깎는다. 편향을 두면 "입력이 이 정도는 되어야 반응한다"는 문턱을 학습으로 조절할 수 있다.

층으로 묶기

뉴런 하나로는 질문 하나밖에 못 던진다. 여러 개를 나란히 두면 여러 질문을 동시에 던질 수 있다. 뉴런 m개를 묶은 것이 선형 계층이고, 각 뉴런의 가중치 벡터를 행으로 쌓으면 행렬이 된다.

y=Wx+b, W[m, n], x는 길이 n, y는 길이 m

M1 세션 9에서 본 그대로다. 행렬의 각 행이 뉴런 하나의 가중치이고, 출력의 각 성분이 그 뉴런의 응답이다. 파라미터 수는 m×n+m이다.

배치 처리

실제로는 입력 하나를 처리하는 일이 거의 없다. 여러 개를 한꺼번에 넣는다. 입력을 [B, n] 행렬로 쌓으면 출력은 [B, m]이 된다. 계산은 Y=XWT+b 형태가 되고, 편향은 브로드캐스팅으로 모든 행에 더해진다.

이 배치 처리가 중요한 이유는 두 가지다. 학습에서는 여러 예제의 gradient를 평균해야 안정적이고, 계산에서는 큰 행렬곱 하나가 작은 행렬곱 여러 개보다 GPU에서 훨씬 효율적이기 때문이다. 뒤쪽 이유는 M8에서 성능을 다룰 때 다시, 그리고 훨씬 중요하게 등장한다.

Transformer 안에서

Transformer에서 파라미터를 가진 부분은 사실상 전부 이 선형 계층이다. 입력을 Q, K, V로 바꾸는 것도, 어텐션 결과를 다시 섞는 것도, FFN의 두 층도, 마지막에 어휘 크기로 펼치는 것도 전부 Wx + b다. 그래서 M4에서 모델 구조를 볼 때 실제로 하는 일은 선형 계층이 몇 개 있고 각각의 shape이 무엇인가를 세는 것이다.

한 가지 덧붙이면, 현대 LLM에서는 편향을 아예 빼는 경우가 많다. 성능 차이가 거의 없으면서 파라미터와 계산이 줄고, 정규화 층이 이미 비슷한 역할을 하기 때문이다. 그래서 y=Wx만 있는 구현을 봐도 놀랄 필요는 없다.

백엔드의 언어로

선형 계층은 학습 가능한 계수를 가진 특성 변환 단계다. 입력 필드들을 가중합해 새로운 파생 필드를 만드는 것인데, 그 가중치를 사람이 정하지 않고 데이터에서 찾는다는 점만 다르다.

확인 질문

  1. 선형 계층 [1024, 4096]의 파라미터 수는 몇 개인가? (편향 포함)
  2. 편향 항이 없으면 무엇이 제약되는가?
  3. 뉴런 하나의 계산을 M1의 내적 개념으로 설명해 보라.
확인

1. 가중치 1024×4096=4,194,304개에 편향 1024개를 더해 4,195,328개다.

2. 입력이 0일 때 출력이 반드시 0이 된다. 반응이 시작되는 문턱을 조절할 수 없어 표현력이 제한된다.

3. 입력 벡터와 뉴런의 가중치 벡터를 내적한 뒤 편향을 더한 것이다. 내적이 방향의 일치도를 재므로, 뉴런은 "입력이 내가 찾는 패턴과 얼마나 닮았는가"를 점수로 낸다.


Session 23활성함수는 왜 필요한가

선형 계층만 쌓으면 아무리 깊어도 한 층과 표현력이 같다. 층 사이에 비선형 함수를 하나 끼워 넣는 순간 이 붕괴가 사라지고, 깊이가 실제로 힘이 된다.

붕괴를 확인하기

M1 세션 9에서 잠깐 언급한 것을 정확히 짚자. 선형 계층 두 개를 연달아 적용하면 이렇게 된다.

y=W2(W1x+b1)+b2=(W2W1)x+(W2b1+b2)

괄호 안의 W2W1은 또 하나의 행렬이고, 뒤의 항도 또 하나의 벡터다. 즉 결과는 W'x + b' 형태, 곧 선형 계층 하나와 완전히 같다. 층을 백 개 쌓아도 마찬가지다. 파라미터는 늘어나지만 표현할 수 있는 함수의 종류는 조금도 늘지 않는다.

그래서 층 사이에 선형이 아닌 함수를 넣는다. 그러면 W2·σ(W1x+b1)+b2가 되고, σ가 비선형이면 이 식은 하나의 선형 변환으로 접히지 않는다. 층을 쌓을수록 표현할 수 있는 함수가 실제로 풍부해진다.

어떤 함수를 쓰는가

역사적으로는 sigmoid와 tanh가 먼저 쓰였다. 출력을 0과 1 사이 또는 −1과 1 사이로 눌러 주는 S자 곡선이다. 그런데 이 함수들에는 심각한 문제가 있었다. 입력이 조금만 크거나 작아지면 곡선이 평평해져 기울기가 0에 가까워진다. M1 세션 20에서 본 기울기 소실이 여기서 발생한다. 층을 몇 개만 쌓아도 앞쪽까지 학습 신호가 가지 않았다.

ReLU가 이 문제를 해결했다. 정의는 놀랄 만큼 단순하다.

ReLU(x)=max(0,x)

음수면 0, 양수면 그대로. 양수 구간에서 기울기가 정확히 1이므로 아무리 큰 값이 들어와도 기울기가 죽지 않는다. 계산도 비교 하나라 극도로 싸다. 이 단순함이 깊은 신경망을 실제로 학습 가능하게 만든 결정적 요인 중 하나다.

다만 음수 구간에서는 기울기가 0이라, 어떤 뉴런의 입력이 항상 음수가 되면 그 뉴런은 영원히 학습되지 않는다. 이것을 죽은 ReLU라고 부른다. 여러 변형이 이 문제를 다룬다.

LLM이 쓰는 것들

현대 LLM에서 주로 보이는 것은 GELU와 SwiGLU다.

GELU는 ReLU를 부드럽게 만든 것이다. 0 근처에서 급격히 꺾이는 대신 완만하게 넘어가고, 음수 쪽에서도 작은 값을 통과시킨다. 미분이 매끄러워 최적화에 유리하다는 것이 채택 이유다.

SwiGLU는 조금 다른 발상이다. 입력을 두 갈래로 나눠 한쪽은 값으로 쓰고 다른 쪽은 게이트로 써서 곱한다. 얼마나 통과시킬지를 데이터에 따라 조절하는 셈이다. 성능이 좋아 최근 모델의 FFN에서 사실상 표준이 되었다.

여기서 실무적으로 알아 둘 점이 하나 있다. SwiGLU는 게이트용 갈래가 따로 필요해서 가중치 행렬이 두 개가 아니라 세 개다. 그래서 같은 은닉 차원이라면 파라미터가 1.5배가 되고, 이를 보정하려고 은닉 차원을 4d 대신 8d/3 정도로 줄여 쓰는 관행이 생겼다. M4에서 모델 파라미터를 손으로 셀 때 이 사실을 모르면 계산이 어긋난다.

백엔드의 언어로

활성함수는 파이프라인 단계 사이의 비선형 게이트다. 이것이 없으면 여러 단계를 하나로 접을 수 있으므로 단계를 나눈 의미가 사라진다. 최적화 관점에서 보면, 접을 수 있는 연속된 단계를 접는 것이 당연한 일이듯이, 접히지 않게 만드는 무언가가 중간에 있어야 단계들이 각자 다른 일을 하게 된다.

확인 질문

  1. 선형 계층 열 개를 쌓은 것이 한 개와 같은 이유를 식으로 설명해 보라.
  2. sigmoid가 깊은 신경망에서 문제가 되었던 이유는?
  3. SwiGLU를 쓰면 파라미터 계산에서 무엇을 주의해야 하는가?
확인

1. W2(W1x+b1)+b2=(W2W1)x+(W2b1+b2)이고, 괄호 안이 각각 하나의 행렬과 벡터이므로 결과는 선형 계층 하나와 같은 형태다.

2. 입력이 조금만 커지거나 작아지면 곡선이 평평해져 기울기가 0에 가까워진다. chain rule에서 이 작은 값들이 곱해지며 앞쪽 층까지 학습 신호가 가지 않는 기울기 소실이 일어난다.

3. 게이트용 갈래가 따로 있어 가중치 행렬이 세 개다. 두 개로 계산하면 파라미터를 과소 추정한다. 그래서 은닉 차원을 4d가 아니라 8d/3 정도로 줄여 쓰는 경우가 많다.


Session 24순전파 — 데이터가 흐르는 길

순전파는 입력이 층을 차례로 통과하며 출력이 되는 과정이다. 여기서 중요한 것은 계산 자체가 아니라 각 단계에서 텐서의 모양이 어떻게 변하는가를 추적하는 습관이다.

흐름 따라가기

간단한 분류 신경망을 예로 들자. 입력이 784차원이고 은닉층이 256차원, 출력이 10개 클래스라고 하자. 배치 크기는 32다.

입력 [32, 784]가 들어온다. 첫 선형 계층 [256, 784]를 지나면 [32, 256]이 된다. 활성함수는 모양을 바꾸지 않으므로 그대로 [32, 256]이다. 둘째 선형 계층 [10, 256]을 지나면 [32, 10]이 된다. 마지막에 softmax를 씌우면 각 행이 10개 클래스에 대한 확률분포가 된다. 여전히 [32, 10]이다.

핵심 규칙은 두 개뿐이다. 선형 계층은 마지막 차원을 바꾼다. 활성함수와 정규화는 모양을 바꾸지 않는다. 이 두 규칙만 있으면 임의의 신경망에서 텐서 모양을 끝까지 따라갈 수 있다.

순전파에서 모양이 변하는 지점

[32, 784][32, 256][32, 10] Linear [256,784]→ 마지막 차원만 바뀐다 ReLU → Linear [10,256]활성함수는 모양 유지 배치 차원 32는 끝까지 그대로 따라간다 — 예제들은 서로 섞이지 않는다
배치 차원은 끝까지 보존된다. 32개 예제가 한 텐서 안에 있지만 서로 영향을 주지 않고 나란히 계산된다. 이 독립성 덕분에 GPU가 이들을 완전히 병렬로 처리할 수 있다. M4의 attention은 이 규칙을 깨는 유일한 연산인데, 배치 안에서가 아니라 시퀀스 안에서 토큰들을 섞는다.

학습과 추론에서 달라지는 것

같은 순전파라도 학습 중일 때와 추론할 때 두 가지가 다르다.

첫째, 학습 중에는 역전파를 위해 중간 결과를 전부 저장해 둬야 한다. 각 층의 출력값이 있어야 그 층의 gradient를 계산할 수 있기 때문이다. 이 저장 공간을 활성값 메모리라고 부르고, 큰 모델을 학습할 때 GPU 메모리를 잡아먹는 주범 중 하나다. 추론할 때는 이것이 필요 없어서 같은 모델이라도 메모리가 훨씬 덜 든다. M8에서 VRAM을 계산할 때 이 차이가 핵심이 된다.

둘째, dropout처럼 학습 중에만 동작하는 층이 있다. 추론 시에는 꺼야 하는데, 이것을 잊으면 출력이 매번 달라지고 성능도 떨어진다. 프레임워크에서 학습 모드와 평가 모드를 구분하는 이유가 이것이다.

백엔드의 언어로

순전파는 여러 변환 단계를 통과하는 데이터 파이프라인이고, 각 단계에서 스키마가 어떻게 바뀌는지 아는 것이 디버깅의 절반이다. 스키마 불일치로 파이프라인이 깨지듯이, 신경망에서도 오류의 상당수가 텐서 모양 불일치다.

그리고 학습 시 활성값 저장은 롤백을 위해 중간 상태를 보관하는 것과 같은 성격이다. 되돌아가려면 지나온 상태가 필요하고, 그 보관 비용이 만만치 않다.

확인 질문

  1. 배치 64, 입력 차원 512인 텐서가 [2048, 512] 선형 계층을 지나면 모양이 어떻게 되는가?
  2. 학습할 때와 추론할 때 메모리 사용량이 크게 차이 나는 이유는?
  3. 배치 차원이 끝까지 보존된다는 것은 무엇을 뜻하는가?
확인

1. [64, 2048]이 된다. 선형 계층은 마지막 차원만 바꾸고 배치 차원은 유지한다.

2. 학습 중에는 역전파에 필요한 각 층의 중간 출력을 전부 저장해야 하기 때문이다. 추론에서는 이 활성값 저장이 필요 없다.

3. 배치 안의 예제들이 서로 영향을 주지 않고 독립적으로 계산된다는 뜻이다. 이 독립성이 GPU 병렬 처리의 근거다.


Session 25손실 함수와 학습 목표

손실 함수는 모델의 출력이 얼마나 틀렸는지를 숫자 하나로 요약한다. 숫자 하나여야 하는 이유는 미분해서 방향을 얻어야 하기 때문이고, 무엇을 손실로 정하느냐가 모델이 무엇을 배울지를 전부 결정한다.

왜 하나의 숫자인가

학습은 손실을 줄이는 방향으로 파라미터를 미는 일이다. 그러려면 M1 세션 20에서 본 gradient가 필요하고, gradient는 스칼라를 여러 변수로 미분한 것이다. 출력이 여러 개면 어느 쪽으로 갈지 정할 수 없다. 그래서 아무리 복잡한 목표라도 마지막에는 하나의 숫자로 합쳐야 한다.

여러 목표를 동시에 추구할 때는 가중합으로 합친다. 예를 들어 M6의 선호 최적화에서는 "보상을 높이되 원래 모델에서 멀어지지 마라"는 두 목표를 계수를 붙여 더한다. 이 계수가 두 목표 사이의 균형을 정하는 손잡이가 된다.

언어 모델의 손실

M1 세션 17에서 이미 본 그대로다. 각 위치에서 정답 토큰에 모델이 준 확률의 로그에 마이너스를 붙이고, 모든 위치에 대해 평균한다.

L=(1/N)Σlogq(정답토큰)

여기서 짚어 둘 것이 두 가지 있다.

첫째, 평균의 단위가 토큰이다. 문장 단위가 아니라 토큰 단위로 평균하므로 긴 문서가 짧은 문서보다 손실에 더 크게 기여하지 않는다. 다만 이 때문에 M1 세션 19에서 본 토크나이저 의존성이 생긴다.

둘째, 어떤 토큰을 평균에 포함할지가 선택 사항이다. 사전학습에서는 모든 토큰을 포함한다. 그런데 M6의 SFT에서는 사용자가 쓴 프롬프트 부분을 평균에서 제외한다. 포함하면 모델이 "질문을 생성하는 법"까지 배우기 때문이다. 이 선택 하나가 결과를 크게 바꾸고, 그래서 M6에서 loss masking을 따로 다룬다.

손실이 목표를 정의한다

이 세션에서 가장 중요한 문장은 이것이다. 모델은 손실 함수에 적힌 것만 배운다.

언어 모델의 손실에는 "사실이어야 한다"는 조건이 없다. 그럴듯한 다음 토큰을 맞히라고만 적혀 있다. 참인 문장이 대체로 그럴듯하기 때문에 결과적으로 상관관계가 높지만, 두 목표는 다르다. 이 차이를 잊으면 "왜 모델이 자신 있게 틀린 말을 하는가"라는 질문에 답할 수 없다. 모델은 자신 있게 틀리도록 벌점을 받은 적이 없다. 그럴듯하지 않은 말을 하면 벌점을 받았을 뿐이다.

같은 논리가 사후학습에도 적용된다. 사람의 선호를 손실에 넣으면 모델은 선호받는 답을 낸다. 그런데 사람이 긴 답을 선호하는 경향이 있다면 모델은 이유 없이 길게 쓰는 법을 배운다. 이것이 M6에서 다룰 length bias이고, 손실이 목표를 정의한다는 원칙의 직접적인 귀결이다.

백엔드의 언어로

손실 함수는 최적화 대상 지표다. 그리고 익숙한 교훈이 그대로 적용된다. 지표를 정하면 시스템은 그 지표를 올리는 방향으로 움직이고, 지표가 진짜 목적의 불완전한 대리물이면 그 간극을 파고든다. 여기서는 그 현상이 훨씬 직접적이고 빠르게 나타난다. 손실 함수가 곧 시스템이 추구하는 것의 전부이기 때문이다.

확인 질문

  1. 손실이 반드시 스칼라 하나여야 하는 이유는?
  2. 언어 모델의 손실 함수에 "사실이어야 한다"는 조건이 없다는 것은 실무에서 무엇을 뜻하는가?
  3. SFT에서 프롬프트 구간을 손실 평균에서 빼는 이유는?
확인

1. 학습은 손실을 파라미터로 미분한 gradient의 반대 방향으로 움직이는 일인데, gradient는 스칼라를 미분해야 정의되기 때문이다. 출력이 여러 개면 어느 방향으로 갈지 결정할 수 없다.

2. 사실 정확성은 학습 목표에 들어 있지 않으므로 보장되지 않는다는 뜻이다. 모델은 그럴듯함을 최적화했을 뿐이고, 사실성은 별도로 측정하고 별도로 장치를 걸어야 한다.

3. 포함하면 모델이 프롬프트, 즉 질문을 생성하는 법까지 학습하기 때문이다. 우리가 원하는 것은 답을 생성하는 능력이므로 답 구간의 손실만 쓴다.


Session 26역전파

역전파는 chain rule을 계산 그래프 위에서 뒤에서부터 한 번만 훑어 모든 파라미터의 gradient를 얻는 절차다. 신경망 학습이 현실적으로 가능한 이유가 이 알고리즘 하나에 있다.

왜 뒤에서부터인가

M1 세션 20에서 방향만 언급하고 넘어간 것을 여기서 정확히 짚는다.

신경망을 함수의 합성으로 보자. 파라미터가 θ, 손실이 L이다. θL에 미치는 영향은 chain rule에 따라 중간 단계 미분들의 곱이다. 문제는 이 곱을 어느 방향으로 계산하느냐다.

앞에서부터 곱하면 각 파라미터마다 전체 경로를 따라가야 한다. 파라미터가 80억 개면 80억 번의 순회가 필요하다.

뒤에서부터 곱하면 사정이 완전히 달라진다. 손실에서 출발해 한 층씩 거슬러 올라가는데, 각 층에서 "지금까지 누적된 gradient"를 받아서 그 층의 파라미터 gradient를 계산하고, 동시에 아래층으로 넘길 gradient를 만든다. 한 번의 역방향 통과로 모든 파라미터의 gradient가 나온다.

이 비대칭의 근원은 출력이 스칼라 하나라는 사실이다. 뒤에서 시작하면 처음부터 크기가 1인 것에서 출발하므로 중간에 큰 행렬을 만들 필요가 없다. 앞에서 시작하면 각 파라미터에 대해 따로 추적해야 한다.

한 번의 왕복으로 끝나는 학습 한 스텝

입력층 1층 2 층 3손실 ① 순전파 — 값을 앞으로, 중간 결과를 저장하며 ② 역전파 — 기울기를 뒤로, 한 번의 통과로 전부 ③ 옵티마이저 — 각 파라미터를 기울기 반대 방향으로 한 걸음 이 세 단계가 학습 한 스텝이고, 수십만 번 반복된다
순전파에서 저장해 둔 중간 결과가 역전파에서 소비된다. 그래서 학습은 추론보다 메모리를 훨씬 많이 쓴다. 활성값을 버리고 필요할 때 다시 계산하는 기법(activation checkpointing)이 존재하는 이유가 이 저장 비용이다.

자동미분

실무에서 역전파를 직접 구현할 일은 없다. 프레임워크가 순전파를 지켜보면서 계산 그래프를 기록해 두었다가, 손실에서 backward()를 부르면 자동으로 거꾸로 훑는다. 이것이 자동미분이다.

여기서 알아 둘 실무 사항이 두 가지 있다.

계산 그래프를 기록하는 데 메모리가 든다. 추론할 때는 이 기록이 필요 없으므로 끄는 것이 맞고, 그래서 추론 코드는 gradient 계산을 비활성화한 문맥 안에서 돌린다. 이것을 빼먹으면 메모리가 몇 배로 늘고 속도도 느려진다.

gradient는 기본적으로 누적된다. 새로 계산할 때 이전 값을 덮어쓰지 않고 더한다. 이 동작은 gradient accumulation, 즉 여러 미니배치의 gradient를 모아 큰 배치처럼 학습하는 기법을 가능하게 하지만, 매 스텝 초기화를 잊으면 이전 배치의 gradient가 섞여 학습이 이상해진다. 초보자가 가장 흔히 겪는 버그 중 하나다.

흔히 틀리는 지점

백엔드의 언어로

역전파는 의존성 그래프의 역방향 순회로 기여도를 배분하는 알고리즘이다. 그리고 그 효율의 핵심은 공유되는 중간 결과를 한 번만 계산해 재사용한다는 점인데, 이것은 동적 계획법과 같은 발상이다. 순진하게 계산하면 지수적으로 폭발하는 것을 메모이제이션으로 선형으로 만드는 구조다.

확인 질문

  1. gradient를 뒤에서부터 계산하는 것이 앞에서부터 계산하는 것보다 압도적으로 싼 이유를 출력의 차원으로 설명해 보라.
  2. 추론 코드에서 gradient 계산을 꺼야 하는 이유는?
  3. 매 스텝 gradient를 초기화하지 않으면 무슨 일이 생기는가?
확인

1. 출력인 손실은 스칼라 하나이고 입력인 파라미터는 수십억 개다. 뒤에서 시작하면 크기 1에서 출발해 한 번의 역방향 통과로 전부 얻지만, 앞에서 시작하면 파라미터마다 따로 전체 경로를 추적해야 한다.

2. 계산 그래프를 기록하는 데 메모리가 들고 속도도 느려지는데, 추론에서는 그 기록이 전혀 쓰이지 않기 때문이다.

3. gradient가 기본적으로 누적되므로 이전 배치의 값이 남아 섞인다. 실질적으로 잘못된 방향으로 파라미터가 갱신되어 학습이 망가진다.

앉아서 읽을 것

CS231n의 Backpropagation 노트는 이 주제에 대한 가장 좋은 입문 자료다. 계산 그래프에 숫자를 넣어 손으로 따라가는 예제가 있는데, 한 번 따라가 보면 이 세션의 내용이 몸에 남는다.


Session 27옵티마이저 — SGD에서 AdamW까지

gradient는 방향만 알려 준다. 얼마나, 어떤 관성으로 갈지를 정하는 것이 옵티마이저다. Adam 계열이 표준이 된 대가로 파라미터당 메모리가 세 배가 되었고, 이 사실이 파인튜닝 전략 전체를 규정한다.

SGD와 모멘텀

가장 단순한 갱신은 gradient 반대 방향으로 학습률만큼 가는 것이다.

θθηg

이것이 SGD다. 문제는 손실 지형이 방향마다 가파른 정도가 다를 때 생긴다. 좁고 긴 골짜기 같은 지형에서는 가파른 쪽으로 심하게 진동하면서 완만한 쪽으로는 거의 나아가지 못한다.

모멘텀이 이 문제를 다룬다. 매 스텝의 gradient를 그대로 쓰는 대신, 과거 gradient의 지수이동평균을 유지하고 그 방향으로 간다.

mβm+(1β)g, θθηm

진동하는 성분은 방향이 계속 바뀌므로 평균에서 상쇄되고, 일관되게 같은 방향인 성분은 누적되어 커진다. 골짜기를 굴러 내려가는 공에 관성이 붙는 그림이 흔히 쓰이는 비유다.

Adam

Adam은 여기에 한 가지를 더한다. 파라미터마다 학습률을 다르게 가져가는 것이다.

방법은 이렇다. gradient의 1차 모멘트 m(방향)과 함께 2차 모멘트 v(크기의 제곱의 이동평균)를 유지한다. 그리고 갱신할 때 mv로 나눈다.

mβ1m+(1β1)g
vβ2v+(1β2)g2
θθη·m/(v+ε)

의미는 이렇다. gradient가 계속 크게 나오는 파라미터는 v가 커지므로 나눗셈에 의해 보폭이 줄어든다. gradient가 작은 파라미터는 v가 작아 보폭이 유지된다. 결과적으로 모든 파라미터가 비슷한 속도로 움직인다. 층마다 gradient 크기가 크게 다른 깊은 신경망에서 이 성질이 결정적이다.

여기에 bias correction이라는 보정이 붙는다. mv를 0에서 시작하면 초기 몇 스텝 동안 값이 실제보다 작게 추정되는데, 이것을 나눗셈으로 보정한다. 이 보정이 없으면 학습 초반에 보폭이 이상하게 작아진다.

AdamW와 weight decay

가중치가 너무 커지지 않게 억제하는 정규화 기법이 weight decay다. 원래 Adam에서는 이것을 손실 함수에 항으로 더했는데, 그러면 이 항의 gradient도 v로 나뉘어 버려서 파라미터마다 감쇠 강도가 달라지는 이상한 일이 벌어졌다.

AdamW는 이 항을 손실에서 빼고 갱신 식에 직접 붙인다.

θθη(m/v)ηλθ

이렇게 하면 감쇠가 모든 파라미터에 균일하게 적용된다. 이름의 W가 이 분리를 뜻하고, 현재 LLM 학습의 사실상 표준이다.

메모리라는 대가

이 세션에서 실무적으로 가장 중요한 부분이다.

Adam 계열은 파라미터마다 mv를 저장해야 한다. 파라미터가 P개면 optimizer state가 2P개다. 여기에 gradient P개까지 더하면, 학습 중에는 파라미터 자체 말고도 3배의 저장 공간이 더 필요하다.

숫자로 보자. 8B 모델을 bf16으로 다룬다면 파라미터가 16GB다. 그런데 optimizer state는 정밀도 문제로 보통 fp32로 유지하므로 mv가 각각 32GB, 합쳐서 64GB다. gradient까지 더하면 총 100GB에 근접한다. 80GB 카드 한 장으로는 전체 파인튜닝이 불가능하다는 결론이 여기서 나온다.

M6과 M8로 이어지는 결론

LoRA가 절약하는 것은 파라미터 수가 아니라 이 optimizer state와 gradient다. 전체 파라미터의 0.1%만 학습 대상으로 두면 m, v, gradient도 그만큼만 있으면 되므로 메모리가 급감한다. "LoRA는 왜 메모리를 아끼는가"에 대한 정확한 답이 이 세션에 있다.

백엔드의 언어로

옵티마이저는 적응형 제어기다. 오차 신호(gradient)를 받아 얼마나 크게 조정할지 정하는데, 과거 이력을 누적해 진동을 줄이고(모멘텀) 신호 크기에 따라 이득을 자동 조절한다(2차 모멘트). PID 제어에서 적분항과 이득 스케줄링을 쓰는 것과 발상이 겹친다.

그리고 그 상태를 유지하는 비용이 있다는 점도 익숙한 구조다. 상태를 가진 컴포넌트는 언제나 메모리를 요구하고, 그 메모리가 확장의 병목이 된다.

확인 질문

  1. Adam의 mv는 각각 무엇을 추적하며, m/v라는 형태는 무엇을 달성하는가?
  2. 파라미터가 70억 개인 모델을 AdamW로 전체 학습할 때 optimizer state만으로 대략 얼마의 메모리가 필요한가? (fp32 기준)
  3. AdamW가 원래 Adam과 다른 점 한 가지를 말해 보라.
확인

1. m은 gradient의 이동평균으로 방향과 관성을 추적하고, v는 gradient 제곱의 이동평균으로 크기를 추적한다. m/v는 gradient 크기로 나눠 주는 효과가 있어 파라미터마다 보폭을 자동 조절하고, 결과적으로 모든 파라미터가 비슷한 속도로 움직이게 한다.

2. mv가 각각 70억 개씩, fp32면 4바이트이므로 7e9×4×2=56GB다. gradient까지 더하면 더 늘어난다.

3. weight decay를 손실 함수의 항으로 넣지 않고 갱신 식에 직접 붙인다. 그래서 감쇠가 v에 의해 왜곡되지 않고 모든 파라미터에 균일하게 적용된다.


Session 28학습률과 warmup

학습률은 한 걸음의 크기다. 너무 크면 발산하고 너무 작으면 영원히 못 간다. Transformer 학습에서 처음에 학습률을 0부터 서서히 올리는 warmup이 사실상 필수인 데는 분명한 이유가 있다.

한 걸음의 크기

경사하강의 갱신 식 θθηg에서 η가 학습률이다. gradient는 방향만 알려 주므로 얼마나 갈지는 이 값이 정한다.

너무 크면 최솟값을 지나쳐 반대편 벽으로 튕겨 나가고, 그다음에는 더 크게 튕기면서 발산한다. 손실이 치솟다가 NaN이 되는 전형적인 실패다. 너무 작으면 손실이 아주 천천히 내려가고, 얕은 골에 빠져 나오지 못한다.

학습률에 따라 달라지는 경로

0.30
손잡이를 옮기고 굴려 보면 세 가지 양상이 나온다. 너무 작으면 스무 걸음을 가도 바닥에 못 닿고, 적당하면 몇 걸음에 도착하며, 너무 크면 골짜기를 넘어 튕기다 발산한다. 실제 학습에서 손실이 갑자기 치솟는 spike는 대개 이 세 번째다.

스케줄

학습률을 처음부터 끝까지 고정하는 경우는 거의 없다. 초반에는 크게 움직여 대략적인 위치를 찾고, 후반에는 작게 움직여 세밀하게 다듬는 것이 유리하다. 그래서 학습률을 시간에 따라 줄여 나가는데 이것을 스케줄이라고 한다. 코사인 곡선을 따라 매끄럽게 줄이는 방식이 흔히 쓰인다.

warmup이 필요한 이유

그런데 LLM 학습에서는 처음에 오히려 학습률을 0부터 서서히 올린다. 보통 전체 스텝의 1에서 5퍼센트 구간을 여기에 쓰고, 그다음부터 스케줄에 따라 줄인다. 이 구간이 warmup이다.

왜 필요한지는 앞 세션의 Adam을 알면 설명된다. Adam은 gradient의 2차 모멘트 v로 나눠서 보폭을 정하는데, 학습 초반에는 v가 몇 개의 샘플로만 추정된 값이라 매우 불안정하다. 어쩌다 작은 v가 추정되면 나눗셈 결과가 폭발하고, 첫 몇 스텝에서 파라미터가 엉뚱한 곳으로 크게 튕겨 나간다. 무작위 초기화 상태의 모델은 이 충격에서 회복하지 못하는 경우가 많다.

warmup은 이 위험한 구간을 작은 보폭으로 지나가게 한다. v가 충분한 샘플로 안정될 때까지 시간을 벌어 주는 것이다.

여기에 더해, 학습 초기의 gradient는 크기가 크고 방향이 일관되지 않다. 이때 큰 걸음을 내디디면 아직 아무것도 배우지 않은 상태에서 파라미터가 크게 망가진다. 이것도 warmup이 막아 준다.

실무 규칙

몇 가지 감각을 정리해 둔다.

배치 크기를 키우면 gradient 추정이 더 정확해지므로 학습률을 함께 올릴 수 있다. 배치를 두 배로 하면 학습률을 2배 또는 2배로 올리는 규칙이 흔히 쓰인다.

손실이 특정 스텝에서 갑자기 치솟는 spike를 만나면, 첫 번째 용의자는 학습률이다. 학습률을 낮추거나 gradient clipping을 걸어 대응한다. clipping은 gradient의 크기가 임계값을 넘으면 방향은 유지하고 크기만 잘라 내는 장치다.

파인튜닝의 학습률은 사전학습보다 한두 자릿수 작다. 이미 좋은 위치에 있는 파라미터를 크게 흔들면 기존 능력이 무너지기 때문이다.

백엔드의 언어로

학습률 스케줄은 적응형 백오프의 반대 방향 버전이다. 재시도 간격을 점차 늘리듯이, 여기서는 조정 폭을 점차 줄인다. warmup은 콜드 스타트 보호에 해당한다. 통계가 충분히 쌓이기 전에 자동 조절 장치를 전력으로 돌리면 잘못된 추정에 과잉 반응하므로, 초기에는 보수적으로 시작해 서서히 정상 동작으로 올린다.

확인 질문

  1. 학습률이 너무 클 때와 너무 작을 때 손실 곡선이 각각 어떻게 보이는가?
  2. Adam을 쓸 때 warmup이 특히 중요한 이유를 v의 관점에서 설명해 보라.
  3. 파인튜닝에서 학습률을 사전학습보다 훨씬 작게 잡는 이유는?
확인

1. 너무 크면 손실이 내려가지 않고 진동하거나 치솟다가 NaN이 된다. 너무 작으면 아주 완만하게 내려가고 어느 지점에서 정체한다.

2. Adam은 gradient 제곱의 이동평균 v로 나눠 보폭을 정하는데, 초반에는 v가 소수의 샘플로만 추정되어 불안정하다. 작은 v가 나오면 나눗셈 결과가 폭발해 파라미터가 크게 튕겨 나간다. warmup이 이 구간을 작은 보폭으로 지나게 한다.

3. 파라미터가 이미 좋은 위치에 있기 때문이다. 큰 걸음을 내디디면 사전학습으로 얻은 능력이 무너진다.


Session 29초기화

학습을 시작할 때 가중치에 어떤 값을 넣느냐가 학습의 성패를 가른다. 핵심 원리는 하나다. 층을 지날 때마다 신호의 크기가 커지지도 작아지지도 않게 분산을 맞추는 것이다.

왜 무작위여야 하고, 왜 아무 무작위나 안 되는가

전부 0으로 초기화하면 어떻게 될까. 같은 층의 모든 뉴런이 완전히 같은 값을 내고, 역전파로 받는 gradient도 같아서 영원히 같은 값을 유지한다. 뉴런이 백 개든 천 개든 하나짜리 층과 다를 바가 없다. 이것을 대칭성 문제라고 부르고, 그래서 무작위 값이 필요하다.

그런데 아무 무작위나 되는 것은 아니다. M1 세션 12에서 본 분산의 성질이 여기서 그대로 작동한다. 출력의 각 성분은 입력 n개와 가중치의 곱을 더한 것이므로, 가중치의 분산이 σ2이고 입력의 분산이 1이라면 출력의 분산은 대략 nσ2 이 된다.

이제 문제가 보인다. σ2을 1로 잡으면 출력의 분산이 입력의 n배가 된다. 층을 지날 때마다 n배씩 커지므로 몇 층만 지나도 값이 폭발한다. 반대로 너무 작게 잡으면 층마다 줄어들어 신호가 사라진다.

분산을 맞추는 처방

해결책은 간단하다. nσ2=1이 되도록, 즉 σ2=1/n이 되도록 초기화한다. 여기서 n은 입력 차원, 즉 fan-in이다.

이것이 초기화 기법들의 공통 뼈대다. Xavier 초기화는 순전파와 역전파를 함께 고려해 2/(fan_in + fan_out)을 쓰고, He 초기화는 ReLU를 염두에 두고 2/fan_in을 쓴다. ReLU가 음수 절반을 0으로 죽이므로 분산이 절반이 되고, 그것을 보상하려고 2를 곱한 것이다.

Transformer에서는 여기에 하나가 더 붙는다. 잔차 연결 때문에 층을 지날 때마다 값이 누적되므로, 층 수가 많으면 마지막 층의 출력이 커진다. 그래서 잔차 경로로 들어가는 출력 투영 가중치를 층 수의 제곱근으로 나눠 초기화하는 관행이 있다. 이 조정 없이 깊은 모델을 학습시키면 초반에 불안정해진다.

실무에서 확인하는 법

초기화가 제대로 됐는지는 두 가지로 확인한다.

첫째는 M1 세션 17에서 강조한 초기 손실이다. 어휘 크기가 V일 때 첫 손실이 logV 근처여야 한다. 이 값보다 크게 벗어나면 출력층 초기화가 잘못됐거나 라벨 정렬이 틀린 것이다. 이 점검은 몇 초면 되고 실패를 아주 일찍 잡아 준다.

둘째는 각 층의 활성값과 gradient의 표준편차를 층별로 찍어 보는 것이다. 층을 지나면서 값이 계속 커지거나 계속 작아지면 초기화 문제다. 층마다 비슷한 크기를 유지해야 정상이다.

백엔드의 언어로

초기화는 시스템의 초기 상태 설정이고, 그 목표는 각 단계의 이득이 1 근처가 되게 만드는 것이다. 다단계 증폭 회로에서 각 단의 이득을 맞추지 않으면 최종 출력이 포화되거나 묻히는 것과 같은 문제다. 여기서 그 이득이 층의 입력 차원에 비례하므로 그것으로 나눠 준다.

확인 질문

  1. 가중치를 전부 0으로 초기화하면 무슨 일이 생기는가?
  2. 입력 차원이 n일 때 가중치 분산을 1/n로 잡는 이유를 분산의 성질로 설명해 보라.
  3. He 초기화가 Xavier보다 2배 큰 분산을 쓰는 이유는?
확인

1. 같은 층의 모든 뉴런이 동일한 출력과 동일한 gradient를 갖게 되어 영원히 같은 값을 유지한다. 층의 뉴런 수가 아무리 많아도 하나짜리 층과 표현력이 같아진다.

2. 출력은 입력 n개의 가중합이고, 독립인 항의 합의 분산은 항의 개수만큼 커진다. 따라서 출력 분산이 대략 n×σ2이 되므로, 이것을 1로 유지하려면 σ2=1/n이어야 한다.

3. ReLU가 음수 입력을 전부 0으로 만들어 출력 분산이 대략 절반이 된다. 그 손실을 보상하려고 초기 분산을 2배로 잡는다.


Session 30정규화 — LayerNorm과 RMSNorm

정규화 층은 각 위치의 벡터를 평균 0, 분산 1로 다시 맞춰 층을 지나면서 값의 크기가 표류하는 것을 막는다. RMSNorm은 여기서 평균 빼기를 생략한 더 싼 변형이고, 최근 LLM의 기본값이다.

무엇을 정규화하는가

앞 세션의 초기화는 학습 시작 시점의 크기를 맞춘다. 그런데 학습이 진행되면 가중치가 변하면서 그 균형이 깨진다. 어떤 층의 출력이 점점 커지면 다음 층이 그것에 맞춰 적응해야 하고, 이 연쇄가 학습을 불안정하게 만든다.

정규화 층은 매 순전파마다 값을 강제로 다시 맞춘다. LayerNorm의 계산은 이렇다. 한 토큰의 벡터를 놓고, 그 벡터 안에서 평균과 분산을 구해 표준화한 뒤, 학습 가능한 스케일과 이동을 적용한다.

LayerNorm(x)=γ·xμσ2+ε+β

여기서 μσ2은 그 벡터 하나의 평균과 분산이다. 배치 안의 다른 예제나 시퀀스의 다른 위치를 보지 않는다. 이 점이 중요하다. 각 토큰이 독립적으로 정규화되므로 배치 크기에 영향을 받지 않고, 추론 시 배치가 1이어도 학습 때와 똑같이 동작한다. BatchNorm이 배치 통계에 의존해 언어 모델에서 쓰기 곤란한 것과 대조된다.

ε은 분산이 0에 가까울 때 0으로 나누는 것을 막는 아주 작은 상수다. γβ는 학습되는 파라미터로, 정규화가 표현력을 과도하게 제약하지 않도록 다시 스케일과 위치를 조절할 여지를 준다.

RMSNorm

RMSNorm은 여기서 평균 빼기를 없앤다. 제곱평균제곱근으로만 나눈다.

RMSNorm(x)=γ·x1nixi2+ε

평균을 빼지 않고 이동 파라미터 β도 없다. 실험적으로 성능 차이가 거의 없으면서 계산이 줄고, 평균을 구하기 위한 추가 순회가 사라져 특히 큰 모델에서 의미 있는 속도 이득이 있다. 그래서 최근 LLM은 거의 RMSNorm을 쓴다.

pre-LN과 post-LN

정규화를 어디에 두느냐가 학습 안정성을 크게 좌우한다. 두 가지 배치가 있다.

원래 Transformer 논문은 잔차를 더한 뒤에 정규화했다. x + Sublayer(x)를 계산하고 그 결과를 정규화하는 것이다. 이것을 post-LN이라고 한다.

현재 표준은 정규화를 먼저 하고 잔차를 더한다. x + Sublayer(LayerNorm(x)) 형태다. 이것이 pre-LN이다.

차이는 잔차 경로에 있다. pre-LN에서는 입력 x가 정규화를 거치지 않고 곧장 출력에 더해지므로, 입력에서 출력까지 아무 변형 없이 통과하는 경로가 존재한다. M1 세션 20에서 본 기울기 소실의 관점에서 보면 이 경로를 따라 gradient가 배율 1로 흐른다는 뜻이다. 그래서 warmup 없이도 학습이 비교적 안정적이고, 층을 아주 깊게 쌓을 수 있다.

post-LN에서는 잔차를 더한 뒤 정규화가 걸리므로 이 통로가 막힌다. 층이 깊어질수록 학습이 불안정해지고, warmup에 크게 의존하게 된다. 이것이 표준이 바뀐 이유다.

백엔드의 언어로

정규화는 각 단계 출력에 대한 자동 스케일 보정이다. 여러 단계를 거치면서 값의 범위가 표류하는 것을 각 단계마다 표준 범위로 되돌린다. 그리고 LayerNorm이 배치를 보지 않는다는 성질은 요청 간 독립성에 해당한다. 다른 요청이 배치에 함께 들어 있든 아니든 결과가 같아야 서빙에서 예측 가능한 동작을 얻는다.

확인 질문

  1. LayerNorm이 평균과 분산을 계산하는 범위는 무엇인가? 왜 그 범위가 서빙에서 중요한가?
  2. RMSNorm이 LayerNorm에서 생략한 것은 무엇인가?
  3. pre-LN이 post-LN보다 깊은 모델에서 안정적인 이유를 잔차 경로로 설명해 보라.
확인

1. 토큰 하나의 벡터 안에서만 계산한다. 배치의 다른 예제나 시퀀스의 다른 위치를 보지 않는다. 그래서 배치 구성이 결과에 영향을 주지 않고, 배치 크기가 1이든 64든 같은 출력이 나온다.

2. 평균 빼기와 이동 파라미터를 생략하고 제곱평균제곱근으로 나누기만 한다.

3. pre-LN에서는 입력이 아무 변형 없이 출력에 더해지는 경로가 존재해 gradient가 배율 1로 흐른다. post-LN은 잔차를 더한 뒤 정규화가 걸려 이 통로가 막히므로, 층이 깊어질수록 기울기 전달이 나빠진다.


Session 31잔차 연결

잔차 연결은 층의 출력에 입력을 그대로 더하는 한 줄짜리 장치다. 이것 하나가 깊은 신경망의 학습을 가능하게 했고, Transformer의 모든 서브층에 붙어 있다.

형태와 의도

y=x+F(x)

F가 어떤 변환이고, 그 결과에 입력 x를 그대로 더한다. 이름의 유래는 이렇다. 이 형태에서 F가 학습하는 것은 출력 전체가 아니라 출력과 입력의 차이, 즉 잔차다. 아무것도 바꿀 필요가 없으면 F가 0을 내면 되고, 이것은 무언가 유용한 변환을 처음부터 찾아내는 것보다 훨씬 쉬운 목표다.

왜 이것이 결정적이었는가

M1 세션 20의 chain rule을 떠올려 보자. 층을 지날 때마다 미분값이 곱해지고, 그 배율이 1에서 벗어나면 층 수만큼 지수적으로 증폭되거나 소멸한다.

잔차 연결이 있으면 이 계산이 달라진다. y=x+F(x)x로 미분하면 1+F'(x)다. 상수 1이 더해진다. 이 1이 있으면 F'가 아무리 작아도 전체 미분이 1 근처를 유지하고, 곱의 사슬이 0으로 붕괴하지 않는다.

잔차 연결이 만드는 gradient 통로

잔차 없음 — 기울기가 모든 변환을 통과한다 F₁F₂F₃F₄ 배율 0.8을 네 번 곱하면 0.41 — 층이 깊어지면 0에 수렴한다 잔차 연결 — 우회로가 배율 1을 보장한다 F₁F₂F₃F₄ 덧셈의 미분은 1 — 이 통로에서는 감쇠가 일어나지 않는다
미분에서 덧셈은 기울기를 양쪽 갈래로 그대로 복사한다. 그래서 잔차 경로는 배율 1의 고속도로가 되고, 변환 경로가 아무리 감쇠해도 앞쪽 층까지 신호가 도달한다. Transformer가 100층 넘게 쌓일 수 있는 이유가 이 한 줄이다.

residual stream이라는 관점

Transformer를 이해하는 현대적인 시각이 여기서 나온다. 잔차 경로를 층들이 공유하는 하나의 버스로 보는 것이다.

각 층은 이 버스에서 현재 값을 읽고, 무언가를 계산해서, 그 결과를 버스에 더한다. 덮어쓰지 않는다. 그래서 어떤 층이 기여한 정보는 이후 모든 층이 볼 수 있고, 어떤 층이 아무 일도 하지 않기로 하면 그냥 0을 더하면 된다.

이 관점은 M4에서 Transformer 블록을 볼 때 다시 쓰인다. 블록 안의 attention과 FFN이 각각 버스에서 읽고 버스에 쓰는 두 개의 독립적인 기여자로 읽히면, 구조가 훨씬 단순하게 보인다.

백엔드의 언어로

잔차 연결은 기본값 통과 경로가 있는 미들웨어 체인이다. 각 미들웨어가 요청을 변형할 수도 있고 그냥 통과시킬 수도 있는데, 여기서는 그 선택이 학습으로 정해진다. 그리고 아무 미들웨어도 개입하지 않으면 요청이 원본 그대로 나가는 것처럼, 모든 F가 0을 내면 입력이 그대로 출력이 된다.

이 성질 덕분에 층을 추가하는 것이 안전해진다. 새 층이 도움이 되지 않으면 0을 학습하면 되므로, 층을 늘려서 성능이 나빠지는 일이 원리적으로 줄어든다.

확인 질문

  1. y=x+F(x)x로 미분하면 무엇이 되며, 그것이 왜 중요한가?
  2. 잔차 연결이 있으면 층을 추가하는 것이 왜 상대적으로 안전한가?
  3. residual stream 관점에서 각 층은 무엇을 하는가?
확인

1. 1 + F'(x)가 된다. 상수 1이 더해지므로 F'가 아무리 작아도 전체 미분이 1 근처를 유지하고, chain rule의 곱의 사슬이 0으로 붕괴하지 않는다.

2. 새 층이 도움이 되지 않으면 F가 0을 내도록 학습하면 되고, 그러면 입력이 그대로 출력이 된다. 즉 층을 추가해도 최소한 원래 성능은 유지할 수 있는 구조다.

3. 공유 버스에서 현재 값을 읽고, 계산 결과를 그 버스에 더한다. 덮어쓰지 않으므로 이전 층의 기여가 보존되고 이후 모든 층이 참조할 수 있다.


Session 32과적합·일반화·정규화 기법

과적합은 모델이 학습 데이터의 우연한 패턴까지 외워 새 데이터에서 성능이 떨어지는 현상이다. LLM 규모에서는 이 문제의 양상이 작은 모델과 다르고, 그래서 대응도 다르다.

정의와 신호

모델의 목표는 학습 데이터에서 손실을 줄이는 것이지만, 진짜 목표는 보지 않은 데이터에서 잘하는 것이다. 이 둘이 갈라지는 지점이 과적합이다.

관측 신호는 명확하다. 학습 손실은 계속 내려가는데 검증 손실이 어느 시점부터 올라간다. 그 갈라지는 지점이 과적합이 시작된 곳이다. 그래서 학습할 때는 반드시 학습에 쓰지 않은 데이터를 떼어 두고 그쪽 손실을 함께 본다.

대응 수단들

LLM에서 달라지는 점

여기서 중요한 반전이 있다. 대규모 사전학습에서는 과적합이 주된 걱정거리가 아니다.

이유는 데이터 규모다. 수조 개 토큰을 한 번, 혹은 두세 번만 통과하는 학습에서 모델이 특정 예제를 외울 기회가 거의 없다. 오히려 문제는 반대편에 있다. 데이터에 비해 모델이 작아서 충분히 학습하지 못하는 경우다. 그래서 대규모 사전학습에서는 dropout을 아예 쓰지 않는 경우가 많고, weight decay도 비교적 약하게 건다.

과적합이 실제 문제가 되는 곳은 파인튜닝이다. 여기서는 데이터가 수천에서 수만 건 규모이고 모델은 여전히 거대하므로, 몇 번만 반복해도 모델이 데이터를 외운다. 증상은 학습한 예제와 조금만 다른 입력에 성능이 급락하는 형태로 나타난다. 그래서 파인튜닝에서는 에폭 수를 적게 잡고(보통 1에서 3), 학습률을 낮추고, 검증 손실을 주의 깊게 본다.

M6에서 LoRA를 다룰 때 이 이야기가 다시 나온다. 학습 가능한 파라미터를 줄이는 것 자체가 과적합을 억제하는 효과도 갖는다.

백엔드의 언어로

과적합은 테스트 데이터에 맞춰 튜닝한 시스템이 실제 트래픽에서 무너지는 것과 같은 구조다. 특정 샘플에서 관측된 패턴에 과도하게 최적화하면 그 샘플 밖에서 성능이 떨어진다. 검증 세트를 따로 두는 것은 스테이징 환경을 따로 두는 것과 같은 규율이고, 그것마저 반복해서 보면 오염된다는 점도 같다.

확인 질문

  1. 과적합의 가장 명확한 관측 신호는 무엇인가?
  2. 대규모 사전학습에서 dropout을 잘 쓰지 않는 이유는?
  3. 파인튜닝에서 에폭 수를 1에서 3 정도로 적게 잡는 이유는?
확인

1. 학습 손실은 계속 내려가는데 검증 손실이 어느 시점부터 올라가는 것이다. 두 곡선이 갈라지는 지점이 과적합의 시작이다.

2. 데이터가 워낙 많아 모델이 특정 예제를 외울 기회가 거의 없기 때문이다. 오히려 데이터에 비해 모델 용량이 부족한 쪽이 문제라서, 학습을 방해하는 규제를 약하게 건다.

3. 데이터는 수천에서 수만 건인데 모델은 거대하므로 몇 번만 반복해도 데이터를 외운다. 외우기 시작하면 학습한 예제와 조금만 달라도 성능이 급락한다.


Session 33학습 곡선 읽는 법

학습 로그는 진단 도구다. 손실 곡선의 모양만 보고도 학습률이 높은지, 데이터가 새는지, 초기화가 틀렸는지를 상당 부분 좁힐 수 있다. 이 세션은 그 대조표다.

먼저 확인하는 세 가지

새 학습을 시작하면 순서대로 확인한다.

증상별 대조표

증상유력한 원인확인 방법
손실이 처음부터 <code>NaN</code>초기화 폭주, 0으로 나누기, 잘못된 데이터첫 배치를 직접 뜯어보고 입력에 <code>inf</code>나 <code>NaN</code>이 있는지 확인
손실이 잘 내려가다 갑자기 치솟음학습률이 높음, 이상치 배치학습률 낮추기, gradient clipping, 해당 스텝의 배치 확인
손실이 전혀 안 내려감학습률이 너무 낮거나 높음, 옵티마이저 초기화 누락학습률을 10배씩 올려가며 손실이 반응하는 구간 탐색
손실이 <code>log V</code>에서 멈춤모델이 균등분포만 내고 있음, gradient가 전달되지 않음gradient norm이 0인지, 파라미터가 학습 대상으로 등록됐는지 확인
첫 손실이 비정상적으로 낮음라벨 누수, 시프트 오류입력과 라벨을 직접 디코딩해 눈으로 대조
학습 손실만 계속 내려가고 검증 손실은 상승과적합에폭 축소, 데이터 증가, 조기 종료
손실이 톱니처럼 진동배치가 너무 작거나 학습률이 큼배치 크기 증가, gradient accumulation

NaN 추적 절차

NaN이 나오면 순서가 있다. 손실에서 처음 발견되지만 발생 지점은 대개 훨씬 앞이므로, 거슬러 올라가야 한다.

먼저 입력 데이터를 확인한다. 데이터 자체에 infNaN이 있으면 나머지 조사가 무의미하다. 그다음 프레임워크의 이상 탐지 모드를 켜서 어느 연산에서 처음 NaN이 나오는지 찾는다. 이 모드는 느리지만 발생 지점을 정확히 짚어 준다. 흔한 원인은 0에 대한 로그, 0으로 나누기, 안정화 없는 softmax, 그리고 학습률 폭주다.

무엇을 로그로 남길 것인가

최소한 다음을 남긴다. 학습 손실과 검증 손실, 학습률(스케줄에 따라 변하므로), gradient norm, 초당 토큰 수, 그리고 GPU 메모리 사용량. 여기에 실험 설정 전체와 코드 커밋 해시를 함께 기록해 둔다.

마지막 항목이 특히 중요하다. 재현할 수 없는 실험은 정보가 아니다. 어제 돌린 실험이 무슨 설정이었는지 모르면 오늘의 비교가 의미를 잃는다. M0 세션 5에서 말한 사고 전환이 여기서 구체적인 습관으로 나타난다.

백엔드의 언어로

학습 곡선 읽기는 지표 기반 장애 진단과 완전히 같은 작업이다. 증상을 관측하고, 가능한 원인을 좁히고, 가장 싼 확인부터 실행한다. 다른 점은 여기서 지표가 손실과 gradient norm이라는 것뿐이다. 이미 갖고 있는 절차적 감각이 그대로 전이되는 영역이다.

확인 질문

  1. 학습을 시작하자마자 확인해야 할 세 가지는?
  2. 첫 손실이 예상보다 훨씬 낮게 나왔다면 무엇을 의심해야 하는가?
  3. 손실이 logV 근처에서 전혀 내려가지 않는다면 무엇을 확인하겠는가?
확인

1. 첫 손실이 logV 근처인지, 배치 하나에 과적합시킬 수 있는지, gradient norm이 정상 범위인지.

2. 라벨 누수다. 정답이 입력에 섞여 들어갔거나 시프트가 잘못되어 모델이 답을 미리 보고 있을 가능성이 높다.

3. 모델이 균등분포만 내고 있다는 뜻이므로 학습 신호가 전혀 전달되지 않는 상태다. gradient norm이 0인지, 파라미터가 옵티마이저에 등록됐는지, gradient 계산이 꺼져 있지는 않은지 확인한다.


Session 34복습 — 학습 한 스텝의 전체 그림

M2에서 배운 것들을 하나의 절차로 꿰어 본다. 학습 한 스텝에서 무슨 일이 벌어지는가를 처음부터 끝까지 말할 수 있으면 이 모듈은 끝난 것이다.

한 스텝을 처음부터

배치 하나를 꺼낸다. 입력 토큰과, 그것을 한 칸 민 정답 라벨이 함께 있다.

입력이 순전파를 시작한다. 임베딩을 거쳐 벡터가 되고, 선형 계층을 지날 때마다 마지막 차원이 바뀐다. 층 사이에는 활성함수가 있어서 여러 층을 쌓는 것이 하나로 접히지 않는다. 각 서브층 앞에는 정규화가 있어 값의 크기를 표준 범위로 되돌리고, 서브층의 출력은 입력에 더해진다. 이 잔차 경로가 나중에 gradient가 흐를 고속도로가 된다. 순전파를 하는 동안 각 층의 중간 결과가 저장되는데, 이것이 학습이 추론보다 메모리를 훨씬 많이 쓰는 이유다.

마지막 층은 어휘 크기만큼의 점수를 낸다. softmax로 확률이 되고, 정답 토큰에 준 확률의 로그에 마이너스를 붙인 것이 손실이다. 첫 스텝이라면 이 값이 logV 근처여야 한다.

이제 역전파가 시작된다. 손실에서 출발해 층을 거꾸로 훑으며 각 파라미터의 gradient를 계산한다. 한 번의 역방향 통과로 전부 나온다. 이때 순전파에서 저장해 둔 중간 결과가 소비된다. gradient가 지나치게 커지면 clipping으로 잘라 낸다.

마지막으로 옵티마이저가 파라미터를 갱신한다. AdamW라면 각 파라미터의 1차 모멘트와 2차 모멘트를 갱신하고, 그 둘로 보폭을 정해 gradient 반대 방향으로 한 걸음 옮긴다. 학습률은 스케줄에 따라 정해지는데, 초반이라면 warmup 구간이라 아직 작다. weight decay가 별도로 적용된다.

gradient를 초기화하고, 다음 배치로 넘어간다. 이것을 수십만 번 반복한다.

자가 점검

  1. 선형 계층만 쌓으면 왜 한 층과 같은가? 그것을 막는 것은 무엇인가? (S23)
  2. 학습이 추론보다 메모리를 훨씬 많이 쓰는 이유 두 가지는? (S24, S27)
  3. 역전파를 뒤에서부터 계산하는 것이 왜 압도적으로 싼가? (S26)
  4. AdamW를 쓸 때 파라미터당 추가로 필요한 메모리는 몇 배이며, 그 사실이 M6의 무엇을 설명하는가? (S27)
  5. Transformer 학습에서 warmup이 사실상 필수인 이유는? (S28)
  6. 가중치 초기화의 분산을 1/n으로 잡는 근거는? (S29)
  7. pre-LN이 표준이 된 이유를 잔차 경로로 설명해 보라. (S30, S31)
  8. 새 학습을 시작했을 때 가장 먼저 확인하는 두 가지는? (S33)
확인

1. 선형 변환의 합성은 다시 선형 변환이므로 여러 층을 하나의 행렬로 접을 수 있다. 층 사이의 비선형 활성함수가 이 붕괴를 막는다.

2. 첫째, 역전파에 쓸 각 층의 중간 결과(활성값)를 순전파 동안 전부 저장해야 한다. 둘째, 옵티마이저가 파라미터마다 1차·2차 모멘트를 유지하고 gradient도 따로 필요하므로 파라미터 외에 약 3배의 저장 공간이 든다.

3. 출력인 손실은 스칼라 하나이고 파라미터는 수십억 개다. 뒤에서 시작하면 한 번의 역방향 통과로 전부 얻지만, 앞에서 시작하면 파라미터마다 전체 경로를 따로 추적해야 한다.

4. 1차·2차 모멘트로 2배, gradient까지 3배다. LoRA가 절약하는 것이 바로 이 부분이다. 학습 대상 파라미터를 크게 줄이면 모멘트와 gradient도 그만큼만 필요하다.

5. Adam이 gradient 제곱의 이동평균으로 나눠 보폭을 정하는데 초반에는 그 추정이 불안정하다. 작은 값이 추정되면 나눗셈 결과가 폭발해 파라미터가 크게 망가지므로, 그 구간을 작은 보폭으로 지나간다.

6. 출력은 입력 n개의 가중합이고 독립인 항의 합의 분산은 항의 개수만큼 커진다. 출력 분산을 1로 유지하려면 가중치 분산이 1/n이어야 한다.

7. pre-LN에서는 입력이 정규화를 거치지 않고 곧장 출력에 더해지는 경로가 있어 gradient가 배율 1로 흐른다. post-LN은 그 통로가 막혀 층이 깊어질수록 불안정해진다.

8. 첫 손실이 logV 근처인지, 그리고 배치 하나에 과적합시킬 수 있는지.

모듈을 마치며

M2에서 배운 것 중 세 가지는 M4의 Transformer 블록에 부품 그대로 들어간다. 정규화, 잔차 연결, 그리고 선형 계층이다. Transformer 블록의 구조식을 미리 적어 보면 이렇다.

xx+Attention(RMSNorm(x))
xx+FFN(RMSNorm(x))

이 두 줄에서 아직 모르는 것은 AttentionFFN뿐이다. 나머지는 이미 안다. 다음 모듈에서 토큰과 임베딩을 다루고, 그다음 모듈에서 이 빈칸이 채워진다.

M3세션 35–4511개

토큰과 임베딩

모델이 글자를 보는 방식을 정한다. 여기서 정해지는 어휘 크기와 토큰 효율이 컨텍스트 비용·다국어 성능·서비스 단가를 전부 결정한다.

Session 35왜 토크나이저가 필요한가

모델은 텍스트를 직접 다루지 못하고 정수 배열만 다룬다. 그 정수를 무엇으로 정할 것인가가 토크나이저의 문제이고, 글자 단위는 너무 길고 단어 단위는 무한하다는 양쪽 벽 사이에서 타협점을 찾는 일이다.

두 개의 극단

먼저 글자 단위를 생각해 보자. 어휘가 아주 작아서 좋다. 영어라면 알파벳과 기호를 합쳐 백 개 남짓이면 되고, 처음 보는 단어도 표현할 수 있다. 문제는 길이다. 같은 문장이 훨씬 많은 토큰이 되고, M1에서 봤듯이 attention 비용은 길이의 제곱으로 자란다. 컨텍스트 창에 담을 수 있는 실제 내용이 크게 줄어든다. 또 하나, 글자 하나에는 의미가 거의 없어서 모델이 의미를 조합하는 데 더 많은 층을 써야 한다.

반대 극단은 단어 단위다. 토큰 하나가 의미 단위와 대응해서 시퀀스가 짧아진다. 그런데 어휘가 폭발한다. 영어만 해도 수십만 단어이고 여기에 복수형, 시제 변화, 오타, 신조어, 고유명사가 계속 추가된다. 게다가 학습 때 못 본 단어는 표현할 방법이 아예 없다. 이것을 OOV 문제라고 부르는데, 실제 서비스에서 치명적이다. 사용자가 새로운 제품명 하나만 써도 모델이 그것을 볼 수 없다.

중간을 택하는 발상

그래서 서브워드라는 타협이 등장한다. 자주 등장하는 덩어리는 하나의 토큰으로 묶고, 드문 것은 더 작은 조각으로 쪼갠다.

예를 들어 "tokenization"은 자주 쓰이지 않으니 token + ization 정도로 쪼갠다. token은 흔하니 하나의 토큰이고, ization도 여러 단어에서 반복되므로 하나의 토큰이 될 만하다. 반대로 "the"나 "은", "는" 같이 극도로 흔한 것은 당연히 하나의 토큰이다.

이렇게 하면 두 마리 토끼를 어느 정도 잡는다. 흔한 것은 짧게 표현되니 시퀀스가 길지 않고, 드문 것도 조각으로 표현할 수 있으니 OOV가 없다. 어휘 크기는 보통 3만에서 20만 사이에서 정한다.

토크나이저가 결정하는 것들

토크나이저는 전처리 유틸리티처럼 보이지만 사실 시스템의 여러 성질을 한꺼번에 규정한다.

백엔드의 언어로

토크나이저는 애플리케이션과 모델 사이의 직렬화 계층이다. 그리고 직렬화 포맷이 대개 그렇듯이, 한번 정하면 바꾸기가 매우 어렵다. 토크나이저를 바꾸면 임베딩 행렬이 무의미해지고 모델을 처음부터 다시 학습해야 한다. 스키마 마이그레이션과 같은 종류의 결정인데 되돌리기가 훨씬 비싸다.

확인 질문

  1. 글자 단위 토큰화의 장점과 치명적인 단점을 각각 하나씩 말해 보라.
  2. 단어 단위 토큰화에서 OOV가 왜 치명적인가?
  3. 모델이 "strawberry에 r이 몇 개인가"에 자주 틀리는 이유를 토크나이저로 설명해 보라.
확인

1. 장점은 어휘가 아주 작고 OOV가 없다는 것이다. 단점은 시퀀스가 훨씬 길어져 attention 비용이 제곱으로 늘고 컨텍스트에 담을 실제 내용이 줄어든다는 것이다.

2. 학습 때 보지 못한 단어를 표현할 방법이 전혀 없기 때문이다. 신조어나 고유명사, 오타 하나만으로도 모델이 그 부분을 볼 수 없게 된다.

3. 모델은 글자를 보지 못하고 토큰 덩어리만 본다. 그 단어가 몇 개의 서브워드로 들어오면 그 안의 개별 글자 수를 셀 근거가 없다. 추론 실패가 아니라 입력 표현의 한계다.


Session 36BPE — 병합으로 어휘를 만든다

BPE는 코퍼스에서 가장 자주 붙어 나오는 두 조각을 하나로 합치는 일을 정해진 횟수만큼 반복해 어휘를 만든다. 어휘를 만드는 절차와 문장을 자르는 절차가 서로 다른 일이라는 점이 핵심이다.

학습 절차

원래 데이터 압축 알고리즘이었던 것을 자연어 처리에 가져온 것이라 이름이 byte pair encoding이다. 절차는 이렇다.

먼저 모든 텍스트를 가장 작은 단위로 쪼갠다. 초기 어휘는 이 최소 단위들의 집합이다. 그다음 코퍼스 전체를 훑으면서 인접한 두 단위의 쌍이 몇 번 등장하는지 센다. 가장 많이 등장한 쌍을 하나의 새 단위로 합치고, 어휘에 추가한다. 이 병합 규칙을 순서대로 기록해 둔다. 그리고 다시 센다. 이 과정을 원하는 어휘 크기에 도달할 때까지 반복한다.

작은 예로 따라가 보자. 코퍼스에 low가 5번, lower가 2번, newest가 6번, widest가 3번 등장한다고 하자. 초기에는 전부 글자 단위로 쪼개져 있다.

가장 흔한 인접 쌍을 세면 es가 9번(newest 6 + widest 3)으로 가장 많다. 이 둘을 합쳐 es를 만든다. 다음으로 est가 역시 9번이므로 est가 된다. 그다음은 lo가 7번(low 5 + lower 2)이라 lo가 되고, 이어서 low가 만들어진다. 이런 식으로 계속된다.

여기서 관찰할 것은 빈도가 높은 덩어리가 자연스럽게 하나의 토큰이 된다는 점이다. 사람이 형태소를 정의해 준 것이 아니라 통계가 만들어 낸 결과인데, 결과적으로 est 같은 의미 있는 접미사가 잡힌다.

병합이 진행되는 과정

같은 코퍼스에서 병합을 반복하면 빈도가 높은 조각부터 순서대로 하나의 토큰이 된다. 병합 규칙은 순서가 있는 목록으로 저장되고, 나중에 문장을 자를 때 이 순서대로 다시 적용된다.

인코딩 절차

학습이 끝나면 병합 규칙 목록이 남는다. 새 문장을 자를 때는 이 목록을 기록된 순서대로 적용한다. 글자 단위에서 시작해 첫 번째 규칙이 적용 가능하면 적용하고, 두 번째 규칙으로 넘어가고, 목록 끝까지 반복한다.

여기서 두 가지가 따라 나온다.

사전 분할

실제 구현에는 한 단계가 더 있다. 병합을 시작하기 전에 텍스트를 정규식으로 미리 나눈다. 이것을 사전 분할이라고 하고, 병합이 이 경계를 넘지 못하게 한다.

이유는 두 가지다. 단어 경계를 넘어 병합되면 이상한 토큰이 생긴다. 예를 들어 . The가 통째로 하나의 토큰이 되는 것은 낭비다. 그리고 공백을 어떻게 다룰지도 여기서 정해진다. GPT 계열은 단어 앞의 공백을 단어에 붙여 처리하는데, 그래서 hello hello(앞에 공백)가 서로 다른 토큰이 된다. 이 사실은 프롬프트를 다룰 때 실제로 문제를 일으킬 수 있다. 문장 끝에 공백을 하나 남기면 다음에 올 토큰의 후보 집합이 통째로 달라진다.

사전 분할 정규식은 토크나이저마다 다르고, 이 차이가 같은 알고리즘을 쓰는 토크나이저들 사이에서도 결과 차이를 만든다.

백엔드의 언어로

BPE 학습은 빈도 기반 사전 구축이고, 인코딩은 순서가 있는 치환 규칙의 반복 적용이다. 규칙 목록이 곧 사전이고, 그 목록의 순서가 의미를 갖는다는 점에서 라우팅 규칙표와 성격이 비슷하다. 위에서부터 순서대로 매칭하고, 순서를 바꾸면 결과가 달라진다.

확인 질문

  1. BPE에서 병합할 쌍을 고르는 기준은 무엇인가?
  2. 병합 규칙을 집합이 아니라 순서 있는 목록으로 저장해야 하는 이유는?
  3. 사전 분할이 하는 일과 그것이 필요한 이유를 말해 보라.
확인

1. 코퍼스에서 인접해 등장한 빈도가 가장 높은 쌍이다.

2. 인코딩할 때 규칙을 기록된 순서대로 적용하기 때문이다. 순서가 바뀌면 같은 문장이 다르게 잘린다.

3. 병합 전에 정규식으로 텍스트를 미리 나눠 병합이 그 경계를 넘지 못하게 한다. 단어 경계를 넘는 무의미한 토큰이 생기는 것을 막고, 공백 처리 방식도 여기서 정해진다.

앉아서 읽을 것

Karpathy의 Let's build the GPT Tokenizer는 이 세션의 내용을 코드로 따라가는 두 시간짜리 영상이다. 직접 치지 않고 보기만 해도 사전 분할과 병합 순서의 미묘함이 훨씬 선명해진다.


Session 37byte-level BPE

최소 단위를 글자가 아니라 UTF-8 바이트로 잡으면 어휘가 256개에서 시작하고, 세상의 어떤 문자열도 표현할 수 있어 OOV가 원천적으로 사라진다. 대신 비영어권 언어의 토큰 효율이 나빠진다.

문제 설정

앞 세션에서 "가장 작은 단위로 쪼갠다"고 했는데, 그 단위를 무엇으로 할 것인가. 글자로 잡으면 문제가 생긴다. 세상에는 유니코드 문자가 십만 개가 넘는다. 이것을 전부 초기 어휘에 넣으면 어휘가 시작부터 거대하고, 그렇다고 일부만 넣으면 넣지 않은 문자가 들어왔을 때 표현할 방법이 없다. 이모지 하나, 희귀 한자 하나가 시스템을 막는다.

byte-level BPE는 최소 단위를 UTF-8 바이트로 잡는다. 어떤 유니코드 문자든 UTF-8로 인코딩하면 1에서 4바이트의 열이 되고, 바이트는 256가지뿐이다. 초기 어휘가 256개면 충분하고, 표현 못 하는 입력이 원리적으로 존재하지 않는다.

비용

공짜는 아니다. 영어 알파벳은 UTF-8에서 1바이트지만, 한글은 3바이트다. 중국어와 일본어도 3바이트다. 이모지는 4바이트다.

즉 같은 한 글자를 표현하는 데 언어마다 초기 조각 수가 다르다. 영어는 글자 하나가 조각 하나로 시작하지만 한글은 글자 하나가 조각 셋으로 시작한다. 병합이 이 차이를 어느 정도 메워 주지만, 병합 규칙은 학습 코퍼스의 빈도로 정해지므로 코퍼스에서 비중이 작은 언어는 병합을 적게 받는다. 결과적으로 토큰 효율 격차가 남는다.

이것이 다음다음 세션에서 다룰 한국어 토큰 비용 문제의 절반이다. 나머지 절반은 코퍼스 비중이다.

되돌릴 수 있다는 성질

byte-level의 또 다른 장점은 디코딩이 항상 성립한다는 점이다. 토큰을 바이트로 펼치고 UTF-8로 해석하면 원문이 나온다. 손실이 없다.

여기서 실무적으로 중요한 세부가 하나 있다. 하나의 문자가 여러 토큰에 걸쳐 있을 수 있다. 한글 한 글자가 3바이트인데 그 바이트들이 서로 다른 토큰에 나뉘어 들어가면, 토큰 하나만 디코딩했을 때는 깨진 바이트가 나온다. 스트리밍 출력에서 글자가 깨져 보이다가 다음 토큰이 오면 정상이 되는 현상이 이것이다. 그래서 스트리밍을 구현할 때는 토큰마다 즉시 디코딩하지 않고 완성된 문자 경계까지 버퍼링해야 한다. M9에서 스트리밍을 다룰 때 다시 나온다.

백엔드의 언어로

byte-level은 인코딩 계층을 가장 낮은 공통 표현으로 내리는 전략이다. 문자 집합을 미리 정의하지 않고 바이트 스트림으로 다루면 어떤 입력도 받을 수 있다. 다만 그 대가로 상위 의미 단위와의 정렬이 깨지고, 경계 처리를 직접 해야 한다. 바이트 스트림을 다뤄 본 사람에게는 익숙한 트레이드오프다.

확인 질문

  1. byte-level BPE에서 초기 어휘 크기가 256인 이유는?
  2. 한글이 영어보다 토큰 효율이 나쁜 이유 중 UTF-8에서 오는 부분을 설명해 보라.
  3. 스트리밍 출력에서 한글이 잠깐 깨져 보이는 현상의 원인과 대응은?
확인

1. 최소 단위를 UTF-8 바이트로 잡기 때문이다. 바이트는 256가지뿐이고, 어떤 유니코드 문자든 바이트 열로 표현되므로 OOV가 원리적으로 없다.

2. 영어 알파벳은 UTF-8에서 1바이트지만 한글은 3바이트다. 글자 하나가 세 개의 조각에서 시작하므로 같은 수의 병합을 받아도 도달하는 압축률이 낮다.

3. 한 글자의 3바이트가 서로 다른 토큰에 나뉘어 들어가면 토큰 하나만 디코딩했을 때 깨진 바이트가 나온다. 토큰마다 즉시 디코딩하지 말고 완성된 문자 경계까지 버퍼링해야 한다.


Session 38SentencePiece와 Unigram

BPE만 있는 것은 아니다. SentencePiece는 공백을 특별 취급하지 않고 문자열 전체를 그대로 다루는 구현이고, Unigram은 병합이 아니라 확률 모델로 어휘를 깎아 내려가는 다른 알고리즘이다.

SentencePiece가 푸는 문제

BPE 구현의 사전 분할은 대개 공백을 단어 경계로 가정한다. 영어에서는 잘 맞지만 일본어나 중국어처럼 띄어쓰기가 없는 언어에서는 이 가정이 무너진다. 한국어도 띄어쓰기 규칙이 느슨해서 완전히 신뢰하기 어렵다.

SentencePiece는 이 가정을 버린다. 공백을 특별한 문자로 치환해 다른 문자와 똑같이 취급하고, 텍스트를 통째로 하나의 문자열로 본다. 그러면 언어별 전처리가 필요 없어지고, 디코딩도 완전히 가역적이 된다. 공백 정보가 토큰 안에 들어 있으므로 복원할 때 원문이 정확히 나온다.

이름 그대로 "문장 조각"을 다루는 도구이고, 알고리즘 자체가 아니라 구현체의 이름이라는 점은 알아 둘 만하다. SentencePiece 안에서 BPE를 쓸 수도 있고 Unigram을 쓸 수도 있다.

Unigram의 다른 접근

BPE가 작은 것에서 시작해 합쳐 올라간다면, Unigram은 반대다. 크게 시작해서 깎아 내려간다.

먼저 후보 조각을 아주 많이 만들어 큰 어휘를 잡는다. 그다음 각 조각에 확률을 부여하고, 어떤 조각을 제거했을 때 전체 코퍼스의 확률이 얼마나 나빠지는지 계산한다. 손해가 가장 적은 조각들을 제거한다. 목표 어휘 크기에 도달할 때까지 이것을 반복한다.

결과적으로 BPE와 성격이 조금 다른 어휘가 나온다. 그리고 Unigram에는 BPE에 없는 성질이 하나 있다. 같은 문장을 여러 방식으로 자를 수 있고 각각에 확률이 매겨진다. 이 성질을 이용해 학습 시 일부러 다른 분할을 섞어 주면 모델이 분할 변화에 강해지는데, 이것을 서브워드 정규화라고 부른다.

실무에서의 선택

직접 토크나이저를 학습시킬 일은 드물지만, 어떤 모델이 어떤 방식을 쓰는지 아는 것은 유용하다. GPT 계열은 byte-level BPE, LLaMA 계열은 SentencePiece 기반 BPE, 일부 모델은 Unigram을 쓴다. 다국어 성능을 비교할 때 이 차이가 결과에 영향을 준다.

더 중요한 실무 규칙은 이것이다. 모델과 토크나이저는 한 쌍이다. 모델의 토크나이저를 다른 것으로 바꾸면 임베딩 행렬의 각 행이 가리키던 의미가 전부 어긋나므로 모델이 무의미해진다. 파인튜닝할 때도, 서빙할 때도 반드시 그 모델의 토크나이저를 써야 한다.

확인 질문

  1. SentencePiece가 공백을 특별 취급하지 않는 것이 어떤 언어에서 이점이 되는가?
  2. Unigram이 BPE와 반대 방향으로 어휘를 만든다는 것은 무슨 뜻인가?
  3. 모델의 토크나이저를 바꾸면 왜 모델이 무의미해지는가?
확인

1. 띄어쓰기가 없거나 불규칙한 언어다. 일본어와 중국어는 띄어쓰기가 없고 한국어도 규칙이 느슨해서, 공백을 단어 경계로 가정하는 전처리가 잘 맞지 않는다.

2. BPE는 작은 단위에서 시작해 자주 붙는 것을 합쳐 올라간다. Unigram은 큰 후보 어휘에서 시작해 제거해도 손해가 적은 조각을 깎아 내려 목표 크기에 도달한다.

3. 임베딩 행렬의 각 행이 특정 토큰 ID에 대응하도록 학습되어 있는데, 토크나이저가 바뀌면 같은 문자열이 다른 ID로 매핑되어 모든 행의 의미가 어긋난다.


Session 39한국어는 왜 토큰이 비싼가

한글은 UTF-8에서 글자당 3바이트인 데다, 대부분 모델의 학습 코퍼스에서 비중이 작아 병합을 적게 받는다. 두 요인이 겹쳐 같은 내용을 담는 데 영어보다 두세 배의 토큰이 든다. 이것은 비용, 컨텍스트 용량, 성능에 동시에 영향을 준다.

두 가지 원인

그래서 한국어 특화 모델이나 다국어 비중을 높인 모델은 한국어 토큰 효율이 눈에 띄게 좋다. 이것은 모델의 지능 차이가 아니라 토크나이저 학습 데이터의 차이다.

같은 내용, 다른 토큰 수

영어 — 44자 / 약 10토큰 한국어 — 26자 / 약 24토큰 글자 수는 한국어가 더 적은데 토큰 수는 두 배 넘게 든다
각 칸이 토큰 하나다. 영어는 단어 하나가 대체로 한두 토큰이지만, 한국어는 한 글자가 통째로 한 토큰이거나 심지어 쪼개지기도 한다. 실제 개수는 모델마다 다르므로 쓰려는 모델의 토크나이저로 직접 재 봐야 한다.

실무에 미치는 영향

무엇을 할 수 있는가

먼저 재 보는 것이다. 쓰려는 모델의 토크나이저로 실제 프롬프트와 문서를 인코딩해 토큰 수를 확인한다. 추정하지 말고 측정한다.

그다음은 모델 선택에 이 지표를 넣는 것이다. 한국어 서비스라면 같은 성능이라도 한국어 토큰 효율이 좋은 모델이 운영 비용에서 유리하다. 이것은 벤치마크 점수에 잘 드러나지 않는 실무 지표다.

프롬프트 설계에서도 여지가 있다. 시스템 프롬프트처럼 매 요청 반복되는 부분은 짧을수록 좋고, M9에서 볼 prefix caching을 쓰면 반복되는 부분의 비용을 크게 줄일 수 있다.

확인 질문

  1. 한국어 토큰 효율이 나쁜 두 가지 원인을 말해 보라.
  2. 128k 컨텍스트 모델에서 한국어 문서를 넣을 때 왜 영어만큼 담기지 않는가?
  3. 모델 선택에서 한국어 토큰 효율을 지표로 삼아야 하는 이유는?
확인

1. 첫째, 한글은 UTF-8에서 글자당 3바이트라 초기 조각 수가 많다. 둘째, 대부분 모델의 학습 코퍼스에서 한국어 비중이 작아 병합 예산이 적게 배정되고, 그래서 잘게 쪼개진 채로 남는다.

2. 컨텍스트 한도는 토큰 단위인데 한국어는 같은 내용에 두세 배의 토큰을 쓰기 때문이다. 실제 담을 수 있는 글자 수가 그만큼 줄어든다.

3. 토큰 효율이 API 비용, 컨텍스트 용량, 생성 지연에 동시에 영향을 주기 때문이다. 벤치마크 점수에는 드러나지 않지만 운영 비용을 직접 좌우한다.


Session 40vocab size가 결정하는 것들

어휘 크기는 하나의 손잡이지만 여러 곳에 동시에 영향을 준다. 시퀀스 길이, 임베딩과 출력층의 크기, 그리고 학습 난이도가 서로 반대 방향으로 움직이므로 어딘가에서 균형을 잡아야 한다.

세 방향의 힘

어휘를 키우면 토큰 하나가 더 많은 글자를 담으므로 시퀀스가 짧아진다. attention 비용이 길이의 제곱이므로 이 효과는 크다.

반면 어휘를 키우면 임베딩 행렬과 출력층이 커진다. 둘 다 크기가 V×d다. 어휘가 10만이고 은닉 차원이 4096이면 각각 4억 개, 둘이 8억 개다. 작은 모델에서는 이것이 전체 파라미터의 큰 비중을 차지한다. 1B 모델이라면 파라미터의 상당 부분이 어휘 관련 행렬인 셈이다.

그리고 어휘를 키우면 각 토큰이 학습 중에 등장하는 횟수가 줄어든다. 드문 토큰은 임베딩이 충분히 학습되지 않고, 그러면 그 토큰이 나오는 문맥에서 성능이 떨어진다.

현실의 선택

대부분의 모델이 3만에서 20만 사이를 쓴다. 최근 경향은 커지는 쪽인데, 두 가지 이유가 있다. 다국어를 제대로 지원하려면 어휘 예산이 더 필요하고, 모델 자체가 커지면서 어휘 행렬의 상대 비중이 줄어 부담이 덜해졌기 때문이다.

weight tying

어휘 관련 행렬이 두 개라는 점에서 절약할 여지가 보인다. 입력 임베딩은 토큰 ID를 벡터로 바꾸고, 출력층은 벡터를 어휘에 대한 점수로 바꾼다. 방향은 반대지만 형태가 같다.

그래서 두 행렬을 하나로 공유하는 기법이 있다. weight tying이라고 부른다. 파라미터가 V×d만큼 줄고, 성능 손실은 대체로 미미하며, 오히려 도움이 되는 경우도 있다. 작은 모델일수록 이득이 크다. 다만 큰 모델에서는 두 역할을 분리하는 편이 낫다는 관측도 있어 항상 쓰이지는 않는다.

파라미터를 세어 보기

이 세션의 내용을 실제로 쓰는 방법은 모델 크기를 손으로 세어 보는 것이다. 어휘 관련 부분은 이렇게 계산한다.

임베딩+출력층=2×V×dquad(tying하면;V×d)

V가 128000이고 d가 4096이면 하나가 약 5.2억이고 둘이면 10.5억이다. 8B 모델에서 이 정도면 전체의 13% 정도다. 반면 1B 모델에 같은 어휘를 쓰면 어휘 행렬만으로 파라미터의 절반이 넘어간다. 그래서 작은 모델은 어휘를 줄이거나 tying을 쓴다.

M4에서 전체 파라미터를 세는 연습을 할 때 이 계산이 첫 번째 항목으로 들어간다.

확인 질문

  1. 어휘를 키울 때 좋아지는 것과 나빠지는 것을 각각 하나씩 말해 보라.
  2. 어휘 5만, 은닉 차원 2048인 모델에서 임베딩과 출력층의 파라미터 합은? tying하면?
  3. 작은 모델일수록 weight tying의 이득이 큰 이유는?
확인

1. 좋아지는 것은 시퀀스 길이가 줄어 attention 비용이 낮아지는 것이다. 나빠지는 것은 임베딩과 출력층이 커지고, 각 토큰의 학습 기회가 줄어 드문 토큰의 표현이 부실해지는 것이다.

2. 각각 50000×2048=1.024이므로 둘이면 약 2.05억이다. tying하면 1.02억으로 절반이 된다.

3. 어휘 관련 행렬의 크기는 모델 크기와 무관하게 V×d로 정해지므로, 전체 파라미터가 작을수록 그 비중이 커진다. 절반 가까이 차지하는 경우도 있어 절약 효과가 크다.


Session 41special token과 chat template

모델에게 "여기부터 사용자 발화" 같은 구조를 알려 주는 것도 결국 토큰이다. chat template은 대화를 그 특수 토큰들이 섞인 하나의 문자열로 펼치는 규칙이고, 이것을 틀리면 성능이 조용히 떨어진다.

특수 토큰

일반 텍스트에서 나올 수 없는 토큰을 몇 개 따로 만들어 구조 표시에 쓴다. 문서의 끝을 나타내는 토큰, 대화에서 각 발화의 시작과 끝을 나타내는 토큰, 패딩 자리를 채우는 토큰 같은 것들이다.

이들이 특별한 이유는 어휘에 명시적으로 추가되고 병합 대상이 아니기 때문이다. 일반 텍스트에서는 절대 생성되지 않으므로 구조 신호로 신뢰할 수 있다.

생성 종료도 이 메커니즘으로 이루어진다. 모델이 종료 토큰을 뽑으면 서버가 생성을 멈춘다. 즉 모델이 언제 멈출지도 학습된 행동이고, 사후학습에서 이 습관을 가르친다. 베이스 모델이 끝없이 이어 쓰는 이유가 여기 있다. 멈추라고 배운 적이 없다.

chat template

대화는 역할과 발화의 목록이다. 시스템, 사용자, 어시스턴트가 번갈아 나온다. 그런데 모델은 토큰 배열 하나만 받으므로, 이 구조를 평평한 문자열로 펼쳐야 한다. 그 규칙이 chat template이다.

예를 들어 어떤 모델은 이렇게 펼친다.

`` <|start|>system\n너는 도움이 되는 비서다<|end|> <|start|>user\n안녕<|end|> <|start|>assistant\n ``

마지막 줄에서 어시스턴트 발화가 시작되는 지점까지만 넣고 끊는다. 그러면 모델이 그 자리부터 이어 쓴다.

핵심은 이 형식이 모델마다 다르다는 점이다. 태그 이름도, 줄바꿈 위치도, 시스템 메시지를 다루는 방식도 다르다. 그리고 이 형식은 사후학습 때 모델이 실제로 학습한 배열이므로, 조금만 달라도 모델이 낯선 입력을 받게 된다.

왜 틀리기 쉬운가

이것이 실무에서 조용한 버그를 만드는 대표적인 지점이다. 이유는 틀려도 동작하기 때문이다.

템플릿을 조금 틀리게 적용해도 모델은 그럴듯한 답을 낸다. 에러가 나지 않는다. 다만 성능이 조금 떨어진다. 벤치마크를 돌리면 몇 퍼센트 낮게 나오는데, 원인이 모델인지 프롬프트인지 템플릿인지 구별하기 어렵다. 파인튜닝 때와 서빙 때 다른 템플릿을 쓰는 실수는 더 나쁘다. 학습한 형식과 다른 형식으로 질문을 받으니 학습 효과가 반감된다.

대응은 단순하다. 직접 만들지 말고 토크나이저가 제공하는 템플릿 적용 함수를 쓴다. 그리고 한 번은 그 결과를 실제 토큰 ID까지 출력해서 눈으로 확인한다. 학습 파이프라인과 서빙 파이프라인 양쪽에서 같은 배열이 나오는지 대조한다.

시스템 프롬프트의 비용

한 가지 실무 사항을 덧붙인다. 시스템 프롬프트는 매 요청마다 붙는다. 한국어로 길게 쓰면 세션 39에서 본 대로 토큰이 많이 들고, 그 비용이 모든 요청에 곱해진다.

다행히 이 부분은 요청마다 동일하므로 M9에서 볼 prefix caching이 그대로 적용된다. 프리픽스가 같으면 KV cache를 재사용해 prefill 비용을 크게 줄일 수 있다. 그래서 변하지 않는 부분을 앞에, 변하는 부분을 뒤에 두는 프롬프트 배치가 성능상 유리하다. 이 원칙은 M10의 RAG에서도 다시 나온다.

확인 질문

  1. 특수 토큰이 일반 텍스트에서 생성되지 않는 것이 왜 중요한가?
  2. chat template을 틀리게 적용했을 때 왜 발견이 늦어지는가?
  3. 프롬프트에서 변하지 않는 부분을 앞에 두는 것이 왜 유리한가?
확인

1. 구조 신호로 신뢰할 수 있기 때문이다. 일반 텍스트에서도 나올 수 있다면 사용자가 그 문자열을 입력해 대화 구조를 위조할 수 있다.

2. 틀려도 에러가 나지 않고 그럴듯한 답이 나오기 때문이다. 성능이 몇 퍼센트 떨어질 뿐이라 모델 문제인지 프롬프트 문제인지 구별하기 어렵다.

3. 공통 프리픽스의 KV cache를 재사용할 수 있어 prefill 비용이 크게 줄기 때문이다. 뒤쪽이 달라지는 것은 상관없지만 앞쪽이 달라지면 재사용이 깨진다.


Session 42임베딩은 무엇을 의미하는가

임베딩은 토큰 ID를 벡터로 바꾸는 조회 테이블이고, 그 조회는 사실 one-hot 벡터와 행렬의 곱이다. 각 행이 무엇을 뜻하는지는 사람이 정하지 않고 학습이 정한다.

조회가 곱셈인 이유

임베딩 행렬은 [V, d] 크기다. 어휘의 각 토큰마다 d차원 벡터 하나가 배정되어 있다. 토큰 ID가 4712번이면 4712번째 행을 꺼내면 된다.

그런데 이것을 다르게 볼 수도 있다. 4712번 자리만 1이고 나머지가 0인 길이 V의 벡터를 만들어 임베딩 행렬과 곱하면, 정확히 4712번째 행이 나온다. 즉 임베딩 조회는 one-hot 벡터와의 행렬곱이고, 구현에서는 그 곱셈을 실제로 하지 않고 인덱싱으로 대신할 뿐이다.

이 관점이 유용한 이유는 임베딩이 신경망의 특별한 부품이 아니라 그냥 선형 계층이라는 것을 알려 주기 때문이다. 학습도 다른 층과 똑같이 된다. 어떤 토큰이 등장한 문맥에서 손실이 발생하면, 그 토큰의 행에만 gradient가 전달되어 갱신된다.

벡터가 의미를 담는다는 말

임베딩 공간에서 비슷한 토큰이 가까이 놓인다는 이야기를 흔히 한다. 정확히 무슨 뜻인지 짚어 두자.

학습 목표는 다음 토큰 예측이다. 비슷한 문맥에서 등장하는 토큰들은 예측에 비슷한 영향을 주므로, 손실을 줄이는 과정에서 비슷한 벡터를 갖게 된다. 즉 "의미가 비슷하다"가 아니라 "쓰이는 자리가 비슷하다"가 정확한 표현이다. 대체로 두 개념이 겹치지만 항상은 아니다. "좋다"와 "나쁘다"는 의미가 반대인데 쓰이는 자리가 비슷해서 임베딩이 가까울 수 있다.

각 차원이 무엇을 뜻하는지는 사람이 정하지 않았고, 대개 해석이 불가능하다. 우리가 알 수 있는 것은 벡터들 사이의 상대적 관계뿐이다.

정적 임베딩과 문맥 임베딩

여기서 중요한 구분이 하나 있다. 임베딩 테이블에서 꺼낸 벡터는 문맥과 무관하게 항상 같다. "밤"이라는 토큰은 어두운 시간을 뜻하든 먹는 것을 뜻하든 같은 벡터로 시작한다.

문맥에 따른 구별은 그다음에 일어난다. Transformer 층을 거치면서 attention이 주변 토큰의 정보를 섞어 넣고, 그 결과 같은 토큰이라도 문맥에 따라 다른 벡터가 된다. 임베딩 테이블은 출발점일 뿐이고, 의미의 대부분은 층을 지나며 만들어진다.

이 구분은 M10에서 중요해진다. 문서 검색에 쓰는 임베딩은 문장 전체를 모델에 통과시킨 뒤 나온 벡터이지 이 테이블의 값이 아니다.

출력층과의 대칭

임베딩 행렬이 [V, d]이고 출력층이 [d, V]라는 점은 세션 40에서 봤다. 출력층이 하는 일을 다시 보면 대칭이 분명해진다. 마지막 은닉 벡터와 출력층의 각 행을 내적해서 어휘마다 점수를 낸다.

출력 점수는 은닉 벡터와 각 토큰 벡터의 내적이다. M1 세션 6의 내적이 여기서 다시 나온다. 은닉 벡터가 어떤 토큰의 벡터와 방향이 비슷할수록 그 토큰의 점수가 높다. 그래서 weight tying, 즉 두 행렬을 공유하는 것이 자연스러운 발상이 된다. 입력에서 쓰는 토큰 표현과 출력에서 비교하는 토큰 표현이 같아야 할 이유가 있는 것이다.

확인 질문

  1. 임베딩 조회가 행렬곱과 같다는 것은 무슨 뜻인가?
  2. "임베딩이 가까우면 의미가 비슷하다"를 더 정확하게 고쳐 말해 보라.
  3. 출력층이 어휘마다 점수를 내는 계산을 내적의 언어로 설명해 보라.
확인

1. 해당 토큰 자리만 1인 one-hot 벡터를 임베딩 행렬에 곱하면 그 토큰의 행이 나온다. 구현에서는 곱셈 대신 인덱싱을 쓸 뿐 수학적으로는 선형 계층이다.

2. "쓰이는 문맥이 비슷하면 임베딩이 가까워진다"가 정확하다. 학습 목표가 다음 토큰 예측이므로 예측에 비슷한 영향을 주는 토큰끼리 비슷한 벡터를 갖게 된다. 의미가 반대인 단어도 쓰이는 자리가 같으면 가까울 수 있다.

3. 마지막 은닉 벡터와 출력층의 각 행(각 토큰에 대응하는 벡터)을 내적한 값이 그 토큰의 점수다. 방향이 비슷할수록 점수가 높다.


Session 43위치 정보는 왜 필요한가

Attention은 토큰들의 집합을 보는 연산이라 순서를 알지 못한다. "개가 사람을 물었다"와 "사람이 개를 물었다"를 구별하려면 위치 정보를 따로 넣어 줘야 한다.

순서가 사라지는 이유

M4에서 자세히 보겠지만 attention의 구조를 미리 한 줄로 말하면 이렇다. 각 토큰이 다른 모든 토큰과 유사도를 계산하고 그 비율대로 섞는다. 이 계산 어디에도 "몇 번째 토큰인가"가 들어가지 않는다.

그래서 입력 토큰의 순서를 섞으면 출력도 같은 방식으로 섞일 뿐, 값 자체는 변하지 않는다. 이 성질을 순열 동변성이라고 부르는데, 집합을 다루는 연산에서는 바람직하지만 언어에서는 치명적이다. 언어는 순서가 의미를 만든다.

RNN에는 이 문제가 없었다. 토큰을 순서대로 처리하니 순서가 구조에 내장되어 있었다. Transformer는 순서를 버리고 병렬성을 얻었으므로, 순서를 다시 주입해야 한다.

세 가지 방식

이 방식에는 한계가 있다. 학습 때 본 최대 위치까지만 벡터가 존재하므로, 더 긴 입력이 들어오면 처리할 수 없다. 그리고 모델이 배우는 것은 절대 위치인데, 언어에서 중요한 것은 대개 상대 위치다. "바로 앞 단어"가 5번째든 500번째든 관계는 같다.

컨텍스트 확장과의 관계

위치 인코딩 방식은 "학습은 4k로 했는데 서비스는 32k로 하고 싶다" 같은 요구와 직결된다. 절댓값을 더하는 방식은 학습 범위를 벗어나면 그냥 동작하지 않는다. RoPE는 회전 주기를 조정하는 방식으로 확장할 여지가 있어서, 학습 후에 컨텍스트를 늘리는 여러 기법이 여기서 나왔다.

이것이 RoPE가 표준이 된 실무적인 이유 중 하나다. 성능이 조금 나은 것보다, 나중에 컨텍스트를 늘릴 수 있다는 성질이 운영에서 더 큰 가치를 갖는다.

확인 질문

  1. attention이 순서를 모른다는 것을 정확히 설명해 보라.
  2. 절대 위치 임베딩의 두 가지 한계는?
  3. RoPE가 표준이 된 실무적 이유를 하나 말해 보라.
확인

1. attention은 각 토큰이 다른 토큰과 유사도를 계산해 섞는 연산인데, 그 계산에 위치가 들어가지 않는다. 따라서 입력 순서를 섞으면 출력도 같은 방식으로 섞일 뿐 값 자체는 달라지지 않는다.

2. 첫째, 학습 때 본 최대 위치까지만 벡터가 존재해 더 긴 입력을 처리할 수 없다. 둘째, 절대 위치를 배우지만 언어에서 중요한 것은 대개 상대 위치다.

3. 학습 이후에 컨텍스트 길이를 확장할 여지가 있다. 회전 주기를 조정하는 방식으로 더 긴 입력을 다룰 수 있어 운영에서 가치가 크다.


Session 44RoPE

RoPE는 위치 정보를 더하지 않고 Q와 K를 위치에 비례해 회전시킨다. 그러면 두 벡터의 내적이 자동으로 두 위치의 차이에만 의존하게 되어, 상대 위치가 구조적으로 반영된다.

발상

앞 세션에서 본 문제를 다시 정리하면 이렇다. attention 점수는 Q와 K의 내적이고, 우리는 이 점수가 두 토큰의 거리를 반영하기를 원한다. 그런데 위치 벡터를 더하는 방식으로는 절대 위치만 들어간다.

RoPE의 착상은 더하지 말고 회전시키자는 것이다. 위치 m에 있는 토큰의 Q를 각도 만큼 회전시키고, 위치 n에 있는 토큰의 K를 각도 만큼 회전시킨다.

여기서 회전의 성질이 결정적으로 쓰인다. 두 벡터를 각각 회전시킨 뒤 내적하면, 그 값은 두 회전각의 차이에만 의존한다. 즉 mθnθ=(mn)θ다. 절대 위치 mn은 사라지고 차이 mn만 남는다.

Rmq,;Rnk=q,;Rnmk

이 등식이 RoPE의 전부다. 위치를 절대적으로 넣었는데 결과는 상대적으로 나온다.

위치에 따라 회전하는 벡터

2
5
두 손잡이를 함께 옮겨 보라. 거리 mn이 같으면 내적이 같다. 위치 2와 5의 관계는 위치 9와 12의 관계와 완전히 동일하다. 이것이 "절대 위치를 넣었는데 상대 위치가 나온다"는 말의 뜻이다.

실제 구현

벡터가 2차원이면 그냥 회전시키면 되지만 실제로는 128차원 같은 고차원이다. RoPE는 이것을 2차원씩 짝지어 각 짝을 독립적으로 회전시킨다. 128차원이면 64개의 2차원 평면이 있는 셈이다.

그리고 짝마다 회전 속도를 다르게 준다. 앞쪽 짝은 빠르게 돌고 뒤쪽 짝은 아주 느리게 돈다. 구체적으로 i번째 짝의 각속도는 대략 이런 식으로 정한다.

θi=1100002i/d

빠르게 도는 짝은 가까운 거리의 미세한 차이를 구별하고, 느리게 도는 짝은 멀리 떨어진 위치까지 구별할 수 있다. 여러 주기의 시계를 겹쳐 놓은 것과 비슷한 구조다.

한 가지 더. RoPE는 임베딩에 더해지는 것이 아니라 attention 안에서 Q와 K에만 적용된다. V에는 적용하지 않고, 매 층마다 다시 적용된다. 이것이 위치 임베딩을 입력에 한 번 더하는 기존 방식과 다른 점이다.

컨텍스트 확장

여기서 세션 43에서 예고한 이야기가 나온다. 학습을 4k 컨텍스트로 했는데 32k로 늘리고 싶다면, 회전 각도가 학습 범위를 넘어가는 것이 문제다.

해결책의 발상은 회전을 느리게 만드는 것이다. 위치를 그대로 넣으면 학습 때 본 적 없는 각도가 나오니, 위치를 8로 나눠 넣으면 32k 위치가 4k 각도 범위 안에 들어온다. 이것을 위치 보간이라고 부른다. 다만 이렇게만 하면 가까운 거리의 해상도가 떨어지므로, 빠르게 도는 짝과 느리게 도는 짝을 다르게 처리하는 개선안들이 나왔다. NTK 스케일링이나 YaRN 같은 이름으로 불리는 것들이다.

세부는 Advanced 영역이지만, 컨텍스트 확장이 공짜가 아니며 대개 소량의 추가 학습을 동반한다는 사실은 실무에서 알아 둘 필요가 있다. "이 모델은 컨텍스트 128k입니다"라는 표기가 그 길이에서 성능이 유지된다는 보증은 아니다.

확인 질문

  1. RoPE에서 내적이 상대 위치에만 의존하게 되는 이유를 회전의 성질로 설명해 보라.
  2. RoPE는 Q, K, V 중 어디에 적용되며 언제 적용되는가?
  3. 차원 짝마다 회전 속도를 다르게 주는 이유는?
확인

1. 두 벡터를 각각 만큼 회전시킨 뒤 내적하면 그 값은 회전각의 차이 (m-n)θ에만 의존한다. 절대 위치가 상쇄되고 거리만 남는다.

2. Q와 K에만 적용하고 V에는 적용하지 않는다. 그리고 입력에 한 번 더하는 것이 아니라 매 층의 attention 안에서 다시 적용된다.

3. 빠르게 도는 짝은 가까운 거리의 미세한 차이를, 느리게 도는 짝은 멀리 떨어진 위치까지 구별하게 하기 위해서다. 여러 주기를 겹쳐 넓은 범위와 높은 해상도를 동시에 확보한다.

앉아서 읽을 것

RoFormer 논문의 3절이 이 세션의 내용을 수식으로 정리한 부분이다. 회전 행렬의 정의와 위 등식의 유도가 두 쪽 안에 들어 있다.


Session 45복습 — 문장 하나가 벡터가 되기까지

이 모듈에서 다룬 것은 문장이 모델에 들어가기 직전까지의 전 과정이다. 한 문장을 예로 그 여정을 처음부터 끝까지 말할 수 있으면 된다.

여정을 따라가기

"나는 어제 학교에 갔다"라는 문장이 들어왔다고 하자.

먼저 토크나이저가 이것을 정수 배열로 바꾼다. 사전 분할 규칙으로 잘게 나눈 뒤, 학습 때 만들어진 병합 규칙을 순서대로 적용해 서브워드로 묶는다. 한국어라 UTF-8에서 글자당 3바이트이고 코퍼스 비중도 작아, 영어 문장보다 토큰이 많이 나온다. 결과는 예를 들어 여덟 개의 정수다.

대화 맥락이라면 이 앞뒤로 특수 토큰이 붙는다. chat template이 역할 구분을 평평한 토큰 배열로 펼치고, 어시스턴트 발화가 시작되는 지점에서 끊는다.

이 정수 배열이 임베딩 테이블을 조회한다. 각 정수가 d차원 벡터가 되어 [T, d] 행렬이 만들어진다. 이 조회는 사실 one-hot 벡터와의 행렬곱이고, 각 행은 학습으로 정해진 값이다. 이 시점의 벡터는 문맥과 무관하다. 같은 토큰은 어디에 있든 같은 벡터다.

여기까지는 순서 정보가 없다. Attention이 집합 연산이라 순서를 못 보기 때문에 위치 정보를 넣어야 한다. 현대 모델은 RoPE를 쓰는데, 입력에 더하는 것이 아니라 각 층의 attention 안에서 Q와 K를 위치만큼 회전시킨다. 그 결과 attention 점수가 두 토큰의 거리에만 의존하게 된다.

이제 [T, d] 행렬이 첫 번째 Transformer 블록으로 들어간다. 그 안에서 무슨 일이 벌어지는지가 다음 모듈이다.

자가 점검

  1. 서브워드 토큰화가 글자 단위와 단어 단위의 어떤 문제를 각각 피하는가? (S35)
  2. BPE의 병합 규칙을 순서 있는 목록으로 저장해야 하는 이유는? (S36)
  3. byte-level BPE가 OOV를 없애는 대신 지불하는 비용은? (S37)
  4. 한국어 토큰 효율이 나쁜 두 원인과, 그것이 실무의 어떤 세 가지에 영향을 주는가? (S39)
  5. 어휘 크기를 키울 때 서로 반대로 움직이는 두 가지는? (S40)
  6. chat template을 잘못 적용해도 왜 발견이 늦는가? (S41)
  7. 임베딩 테이블의 벡터와 Transformer를 거친 뒤의 벡터는 무엇이 다른가? (S42)
  8. RoPE가 절대 위치를 넣고도 상대 위치를 얻는 원리는? (S44)
확인

1. 글자 단위의 시퀀스가 너무 길어지는 문제와, 단어 단위의 어휘 폭발 및 OOV 문제를 동시에 피한다. 흔한 덩어리는 하나로 묶고 드문 것은 조각으로 표현한다.

2. 인코딩할 때 규칙을 저장된 순서대로 적용하기 때문이다. 순서가 다르면 같은 문장이 다르게 잘린다.

3. 비영어권 언어의 토큰 효율이다. 한글·한자는 UTF-8에서 3바이트라 더 많은 조각에서 출발한다.

4. 원인은 UTF-8 바이트 수와 학습 코퍼스 비중이다. 영향은 API 비용 증가, 컨텍스트 실질 용량 감소, 긴 출력의 생성 지연 증가다.

5. 시퀀스 길이는 짧아지지만 임베딩·출력층 파라미터는 커지고 토큰당 학습 기회는 줄어든다.

6. 틀려도 에러 없이 그럴듯한 답이 나오고 성능만 조금 떨어지기 때문이다.

7. 임베딩 테이블의 벡터는 문맥과 무관하게 항상 같다. Transformer 층을 거치면서 attention이 주변 토큰 정보를 섞어 넣어 문맥에 따라 다른 벡터가 된다.

8. Q와 K를 각자의 위치만큼 회전시키면, 두 벡터의 내적이 회전각의 차이 즉 위치의 차이에만 의존하게 되기 때문이다.

모듈을 마치며

M3에서 다룬 것은 전부 모델 바깥의 결정이었다. 토크나이저도, 어휘 크기도, 위치 인코딩 방식도 모델 구조 자체는 아니지만 모델이 무엇을 볼 수 있는지를 규정한다. 그리고 이 결정들은 한번 내리면 되돌리기가 매우 비싸다.

다음 모듈이 이 트랙의 척추다. [T, d] 행렬이 Transformer 블록 안에서 무슨 일을 겪는지, 열일곱 번의 이동에 걸쳐 하나도 빠짐없이 따라간다. M1에서 익힌 내적과 softmax와 분산이 전부 거기서 쓰인다.

M4세션 46–6217개

Attention과 Transformer

이 트랙의 척추다. [T, d] 행렬이 블록 안에서 겪는 모든 일을 하나도 빠짐없이 따라간다. 여기서 막히면 진도를 멈추고 돌아와야 한다.

Session 46attention이 필요했던 이유

문장의 어떤 단어를 이해하려면 멀리 떨어진 다른 단어를 봐야 할 때가 있다. RNN은 그 정보를 여러 단계에 걸쳐 전달하다 잃어버렸고, attention은 그냥 직접 가져오기로 했다.

구체적인 문제

"어제 회사에서 만난 그 사람이 오늘 아침에 보낸 메일에 답장을 했는데, 그가 아직 확인을 안 했다."

마지막의 "그가"가 누구인지 알려면 문장 맨 앞의 "그 사람"까지 거슬러 가야 한다. 사이에 스무 개 남짓의 토큰이 있다.

M3 세션 43에서 본 RNN 구조를 떠올려 보자. 정보를 고정 크기 상태 벡터에 담아 한 단계씩 넘긴다. "그 사람"의 정보가 "그가"에 도달하려면 스무 번의 갱신을 거쳐야 하고, 그 사이에 다른 정보들이 계속 밀려 들어온다. 고정 크기 그릇에 계속 새 물을 부으면 처음 것은 희석된다.

M1 세션 20의 관점으로 보면 더 정확하다. 정보 전달이 여러 단계의 곱으로 이루어지므로, 각 단계의 배율이 1보다 조금만 작아도 스무 단계를 거치면 거의 사라진다. LSTM 같은 개선안은 이 배율을 1에 가깝게 유지하는 장치를 넣은 것이지만, 근본적으로 경로가 길다는 사실은 바뀌지 않았다.

발상의 전환

attention의 답은 단순하다. 중간을 거치지 말고 직접 연결하자.

"그가"라는 토큰이 자기 표현을 만들 때, 앞의 모든 토큰을 한꺼번에 보고 그중 관련 있는 것을 골라 가져온다. "그 사람"이 스무 칸 앞에 있든 백 칸 앞에 있든 경로 길이는 똑같이 1이다. 거리에 따른 감쇠가 원리적으로 사라진다.

여기에 두 번째 이득이 따라온다. 각 토큰이 독립적으로 이 작업을 하므로 전부 동시에 계산할 수 있다. RNN처럼 앞 토큰의 결과를 기다릴 필요가 없다. M0 세션 3에서 본 대로, 이 병렬성이 규모 확장을 가능하게 한 결정적 요인이다.

무엇을 골라 가져오는가

"관련 있는 것을 골라 가져온다"는 말을 정확히 만들어야 한다. 세 가지가 필요하다.

첫째, 각 토큰이 무엇을 찾고 있는지를 표현해야 한다. "그가"는 사람을 가리키는 명사를 찾고 있다.

둘째, 각 토큰이 무엇을 제공하는지를 표현해야 한다. "그 사람"은 자신이 사람 명사임을 알려야 한다.

셋째, 실제로 가져올 내용이 있어야 한다.

이 셋이 다음 세션의 Q, K, V다. 그리고 "찾는 것"과 "제공하는 것"이 얼마나 맞는지를 재는 도구는 이미 M1 세션 6에서 배웠다. 내적이다.

백엔드의 언어로

RNN은 순차 파이프라인이고 attention은 전역 조회다. 파이프라인에서는 앞 단계의 출력만 볼 수 있지만, 조회에서는 전체를 대상으로 검색할 수 있다. 표현력은 후자가 훨씬 크고, 비용도 그렇다. 항목이 T개면 조회 쌍이 T2개다.

익숙한 트레이드오프다. 인덱스 없이 전체를 스캔하면 무엇이든 찾을 수 있지만 데이터가 커지면 감당이 안 된다. attention이 겪는 문제가 정확히 이것이고, 이후 모든 최적화가 여기에 대응한다.

확인 질문

  1. RNN에서 먼 거리의 정보가 손실되는 이유를 경로 길이로 설명해 보라.
  2. attention이 RNN보다 규모 확장에 유리했던 결정적 이유는 무엇이었나?
  3. "관련 있는 것을 가져온다"를 구현하려면 각 토큰에 어떤 세 가지가 필요한가?
확인

1. 정보가 고정 크기 상태를 통해 한 단계씩 전달되므로 거리가 멀수록 경로가 길다. 각 단계의 배율이 곱해지면서 신호가 희석되고, 그 사이 새 정보가 계속 들어와 덮인다.

2. 모든 위치의 계산이 서로 독립이라 완전히 병렬로 처리할 수 있다는 점이다. RNN은 앞 토큰의 결과를 기다려야 해서 GPU의 병렬성을 쓸 수 없었다.

3. 무엇을 찾고 있는지(Q), 무엇을 제공하는지(K), 실제로 전달할 내용(V)이다.


Session 47Q · K · V의 역할

각 토큰의 벡터에 세 개의 서로 다른 행렬을 곱해 Q(질의), K(열쇠), V(값) 세 개를 만든다. Q와 K의 내적이 관련도를 정하고, 그 비율대로 V를 섞는다.

셋을 만드는 방법

입력이 [T, d] 행렬이다. 여기에 세 개의 학습된 행렬을 각각 곱한다.

Q=XWQ,quadK=XWK,quadV=XWV

W_Q, W_K, W_V는 각각 [d, d] 크기이고, 결과인 Q, K, V는 전부 [T, d]다. 즉 같은 입력에서 출발한 세 개의 다른 시점이다.

여기서 중요한 것은 이 셋이 서로 다른 행렬로 만들어진다는 점이다. 같은 토큰이라도 "내가 무엇을 찾는가"와 "내가 무엇을 제공하는가"는 다른 정보이므로, 다른 변환이 필요하다.

역할

관련도는 Q와 K의 내적으로 계산한다. M1 세션 6에서 배운 그대로다. 방향이 비슷하면 큰 값, 무관하면 0 근처, 반대면 음수가 나온다.

왜 K와 V를 분리하는가

가장 자주 나오는 의문이 이것이다. 관련도를 K로 재고 내용을 V로 준다면, 굳이 둘을 나눌 필요가 있나.

이유는 찾는 기준과 전달할 내용이 다를 수 있기 때문이다. "그 사람"이라는 토큰을 예로 들면, 그것이 대명사의 참조 대상으로 선택되는 기준은 "사람을 가리키는 명사구"라는 문법적 성질이다. 그런데 선택된 뒤 실제로 전달해야 할 내용은 그 사람이 누구인지에 대한 의미 정보다. 두 정보는 성격이 다르고, 하나의 벡터에 함께 담으면 서로를 방해한다.

분리하면 모델이 "무엇을 기준으로 찾을지"와 "찾은 뒤 무엇을 줄지"를 독립적으로 학습할 수 있다. 이것이 표현력의 실질적인 차이를 만든다.

백엔드의 언어로

익숙한 비유는 조회 시스템이다. Q는 검색어, K는 색인된 키, V는 저장된 값이다. 여기까지는 정확하다.

이 차이가 중요한 이유는 미분 가능성 때문이다. 하나를 고르는 연산은 미분할 수 없어 학습이 안 된다. 부드럽게 섞기 때문에 gradient가 흐를 수 있다.

확인 질문

  1. Q, K, V는 각각 어떤 질문에 대응하는가?
  2. K와 V를 분리하는 이유를 예를 들어 설명해 보라.
  3. attention이 조회 시스템과 결정적으로 다른 점은?
확인

1. Q는 "나는 무엇을 찾는가", K는 "나는 무엇을 제공하는가", V는 "선택되면 무엇을 전달하는가"에 대응한다.

2. 선택 기준과 전달 내용이 다를 수 있기 때문이다. 어떤 토큰이 문법적 성질 때문에 선택되더라도 전달해야 할 것은 의미 정보일 수 있다. 분리하면 두 가지를 독립적으로 학습할 수 있다.

3. 하나의 값을 고르는 것이 아니라 모든 값을 유사도 비율로 가중 평균한다. 이 부드러움 덕분에 미분이 가능해 학습이 성립한다.


Session 48QKᵀ — 모든 쌍의 유사도

모든 Q와 모든 K의 내적을 한 번에 계산하면 [T, T] 행렬이 나온다. 이 행렬의 (i, j) 칸은 i번째 토큰이 j번째 토큰을 얼마나 볼 것인가의 원점수이고, 여기서 attention의 모든 비용이 발생한다.

한 번의 행렬곱으로

토큰 i가 토큰 j를 얼마나 볼지는 qi·kj다. 이것을 모든 ij에 대해 계산해야 한다.

각 쌍을 따로 계산할 필요는 없다. Q가 [T, d], K가 [T, d]이므로 K를 전치해 [d, T]로 만들고 곱하면 [T, T]가 나온다.

S=QKtop,quadSij=qi·kj

행렬곱 한 번에 모든 쌍의 내적이 계산된다. M1 세션 9에서 "행렬곱은 내적을 여러 번 하는 것"이라고 한 것이 여기서 정확히 쓰인다. 그리고 이것이 GPU가 가장 잘하는 형태의 연산이라는 점도 중요하다.

행과 열의 의미

[T, T] 행렬을 읽을 때 축을 헷갈리지 않는 것이 중요하다.

이 비대칭은 실무에서 헷갈리기 쉬운 지점이다. attention 히트맵을 볼 때 "행 방향으로 정규화되어 있다"는 것만 기억하면 된다.

비대칭이라는 성질

S는 대칭 행렬이 아니다. qi·kjqj·ki는 다른 값이다. Q와 K가 다른 행렬로 만들어졌기 때문이다.

이것은 결함이 아니라 필요한 성질이다. 언어에서 관계는 방향이 있다. 대명사가 선행사를 참조하는 것과 선행사가 대명사를 참조하는 것은 다른 일이다.

여기서 비용이 발생한다

[T, T] 행렬은 attention의 모든 비용 문제의 근원이다.

토큰이 1000개면 백만 개의 값이다. 8000개면 6400만 개다. 여기에 배치와 헤드 수가 곱해진다. 배치 8, 헤드 32, 길이 8000이면 8×32×8000×8000으로 약 164억 개의 값이고, 16비트로 저장해도 32GB가 넘는다. 길이를 두 배로 늘리면 이 값이 네 배가 된다.

이 사실 하나가 이후 모든 최적화를 설명한다. M8의 FlashAttention은 이 행렬을 아예 메모리에 만들지 않는 방법이고, sliding window나 sparse attention은 일부만 계산하는 방법이며, M9의 여러 기법도 결국 이 비용을 관리하는 것이다.

확인 질문

  1. [T, d]인 Q와 K로 [T, T] 점수 행렬을 만드는 연산을 써 보라.
  2. 점수 행렬의 행과 열은 각각 무엇을 뜻하며, 어느 쪽에 합이 1이라는 제약이 붙는가?
  3. 시퀀스 길이가 2배가 되면 점수 행렬의 크기는 몇 배가 되는가?
확인

1. K를 전치해 [d, T]로 만든 뒤 Q와 곱한다. S=QKT이고 Sij=qi·kj다.

2.i는 토큰 i가 각 토큰을 얼마나 보는지, 열 j는 토큰 j가 받는 주목이다. softmax는 행 방향으로 적용되므로 행의 합이 1이 된다.

3. 4배다. 행과 열이 각각 2배가 되므로 원소 수는 제곱으로 늘어난다.


Session 49왜 √d로 나누는가

내적의 분산은 차원에 비례해 커진다. 그 값을 그대로 softmax에 넣으면 분포가 한쪽으로 포화되어 기울기가 사라진다. 표준편차인 d로 나눠 분산을 1로 되돌리는 것이 유일한 목적이다.

M1의 계산을 다시

이 질문의 답은 전적으로 M1 세션 12에 있다. 여기서 확정한다.

qk가 각각 d차원이고, 각 성분이 평균 0, 분산 1이라고 하자. 초기화를 그렇게 맞추기 때문에 학습 초기에는 대략 맞는 가정이다.

q·k=i=1dqiki

각 항 qiki의 분산이 1이고 항들이 독립이라면, 독립인 항의 합의 분산은 항의 개수만큼 커진다. 따라서 이 합의 분산은 d이고 표준편차는 d다.

d가 64면 표준편차 8, 128이면 약 11.3, 512면 약 22.6이다. 즉 헤드 차원이 클수록 점수의 진폭이 커진다.

무엇이 망가지는가

M1 세션 14에서 본 softmax의 성질을 떠올리자. softmax는 입력의 차이에 지수적으로 반응한다. 두 점수의 차이가 10이면 확률 비율이 e122026배다. 차이가 20이면 사실상 1등이 전부 가져간다.

표준편차가 11.3이면 점수들이 대략 ±34 범위에 퍼진다. 이 상태에서 softmax를 적용하면 거의 항상 한 토큰에 확률이 몰린다. 두 가지가 망가진다.

처방

분산이 d인 값을 d로 나누면 분산이 1로 돌아온다. 분산은 상수배에 대해 제곱으로 반응하므로 (1/d)2×d=1이다.

attentionscore=QKtopdhead

여기서 d는 전체 모델 차원이 아니라 헤드 하나의 차원이다. 다음 세션들에서 볼 multi-head 구조에서 각 헤드는 d_head = d / H 차원을 다루고, 내적도 그 차원에서 이루어지기 때문이다. 이것을 헷갈려 전체 d로 나누면 스케일이 어긋난다.

스케일링을 뺐을 때 벌어지는 일

16
같은 Q, K로 계산한 attention 분포다. 스케일링 없이 d를 키우면 분포가 한 칸으로 무너진다. 그 상태에서는 다른 토큰을 볼 수 없을 뿐 아니라, softmax의 기울기가 0에 가까워 학습 자체가 멈춘다. 버튼을 눌러 d로 나누면 d를 아무리 키워도 분포가 유지된다.

확인 질문

  1. 내적의 분산이 d가 되는 이유를 한 줄로 설명해 보라.
  2. d로 나누지 않으면 학습이 멈추는 과정을 두 단계로 말해 보라.
  3. 여기서 d는 모델 차원인가 헤드 차원인가? 왜 그런가?
확인

1. 분산이 1인 독립적인 항 d개를 더한 값이므로 합의 분산이 d가 된다.

2. 첫째, 점수의 진폭이 커져 softmax가 한 토큰에 확률을 몰아주며 포화된다. 둘째, 포화된 지점에서는 softmax의 기울기가 0에 가까워 역전파로 전달할 학습 신호가 사라진다.

3. 헤드 차원이다. 내적이 각 헤드 안에서 d_head 차원 벡터끼리 이루어지므로 분산도 그 차원에 비례한다.


Session 50causal mask

언어 모델은 다음 토큰을 예측해야 하므로 미래를 보면 안 된다. 마스킹은 미래 위치의 점수에 -∞를 더해 softmax 이후 정확히 0이 되게 만드는 방식으로 구현된다. 곱해서 0으로 만드는 것이 아니다.

왜 필요한가

M0 세션 1에서 본 대로 학습은 병렬로 이루어진다. 문장 전체를 한 번에 넣고 모든 위치에서 동시에 다음 토큰을 예측한다.

문제는 attention이 모든 토큰을 볼 수 있다는 점이다. 3번째 위치에서 4번째 토큰을 예측해야 하는데, attention이 4번째 토큰을 그냥 볼 수 있다면 예측이 아니라 복사다. 손실은 0에 가까워지지만 아무것도 배우지 못한다.

이것이 M2 세션 33의 대조표에 있던 "첫 손실이 비정상적으로 낮음" 증상의 한 원인이다.

-∞를 더한다

해결책은 점수 행렬에서 미래에 해당하는 칸을 무효화하는 것이다. 구현이 중요하다.

SijSij+Mij,quadMij=begincases0&ji\&j>iendcases

-∞더한다. 그러면 softmax에서 exp()=0이 되어 그 항의 확률이 정확히 0이 되고, 남은 항들끼리 정규화되어 합이 1이 된다.

여기서 자주 나오는 오해를 짚는다. softmax 이후에 0을 곱하면 안 된다. 그러면 남은 확률의 합이 1이 아니게 된다. 미래 토큰이 이미 확률을 가져간 상태에서 그것만 지우는 셈이라, 나머지 확률의 총합이 1보다 작아진다. 반드시 softmax 이전에 더해야 한다.

실제 구현에서는 -∞ 대신 아주 큰 음수(예: 부동소수점 최솟값)를 쓴다. -∞를 그대로 쓰면 어떤 연산에서 NaN이 발생할 수 있기 때문이다. 특히 첫 번째 토큰처럼 볼 수 있는 것이 자기 자신뿐인 행에서 전부 -∞가 되면 곤란해진다.

하삼각 구조

마스크를 적용한 점수 행렬은 아래쪽 삼각형만 값이 있는 형태가 된다. 첫 행은 자기 자신만, 두 번째 행은 앞의 둘만, 하는 식이다.

이 구조가 만드는 결과 하나를 짚어 두자. 첫 번째 토큰은 자기 자신밖에 볼 수 없다. 그래서 첫 토큰의 표현은 attention으로 얻을 것이 거의 없고, 이 자리가 특별하게 동작하는 현상이 관측된다. 실제로 많은 모델에서 첫 토큰이 "아무것도 보고 싶지 않을 때 대신 보는 자리"로 쓰인다는 분석이 있는데, 여기서 비롯된 성질이다.

인코더와의 차이

번역이나 문장 분류 같은 작업에서는 전체 문장을 다 보는 것이 자연스럽다. 이런 모델에서는 마스크를 걸지 않고 모든 토큰이 서로를 본다. 이것을 양방향 attention이라고 하고, BERT 계열이 이 방식이다.

GPT 계열이 causal mask를 쓰는 것은 생성 모델이기 때문이다. 생성은 한 토큰씩 진행되므로 학습도 같은 조건에서 해야 한다.

이 구분은 M10에서 다시 중요해진다. 검색용 임베딩 모델은 대개 양방향이고, 그래서 문장 전체를 한 벡터로 요약하는 데 유리하다.

확인 질문

  1. 마스킹을 softmax 이전에 더하는 방식으로 하는 이유는?
  2. 실제 구현에서 -∞ 대신 큰 음수를 쓰는 이유는?
  3. causal mask를 걸지 않고 언어 모델을 학습시키면 어떤 증상이 나타나는가?
확인

1. softmax 이전에 -∞를 더하면 exp가 0이 되어 그 항이 제외된 채로 나머지가 정규화된다. 이후에 0을 곱하면 남은 확률의 합이 1이 되지 않는다.

2. -∞가 연산 중 NaN을 유발할 수 있기 때문이다. 특히 한 행이 전부 -∞가 되는 경우가 문제가 된다.

3. 손실이 비정상적으로 낮게 나온다. 모델이 예측해야 할 토큰을 직접 볼 수 있으므로 예측이 아니라 복사가 되고, 학습이 이루어지지 않는다.


Session 51softmax와 가중합

마스킹된 점수에 행 단위로 softmax를 적용하면 각 토큰이 다른 토큰들에 배분한 주의의 비율이 나온다. 그 비율대로 V를 가중 평균한 것이 그 토큰의 새 표현이다.

두 단계

점수 행렬 S가 준비됐다. 여기에 행 방향 softmax를 적용한다.

A=softmaxleft(QKtopdh+Mright)

A[T, T]이고, 각 행의 합이 정확히 1이다. A_ij는 토큰 i가 토큰 j에 배분한 주의의 비율이다.

그다음 이 비율로 V를 섞는다.

O=AV

A[T, T], V[T, d_h]이므로 결과는 [T, d_h]다. 출력의 i번째 행은 이렇게 계산된 것이다.

oi=jAijvj

V들의 가중 평균이고, 가중치의 합이 1이므로 진짜 평균이다. 출력 벡터는 어떤 개별 V와도 다른, 새로 조합된 벡터다.

무엇이 일어났는가

이 두 줄이 attention의 전부다. 다시 정리하면 이렇다.

각 토큰이 자기 Q로 모든 K를 조회해 관련도 점수를 얻고, 미래를 가리고, 점수를 확률로 바꾸고, 그 확률대로 다른 토큰들의 V를 섞어서 자기 표현을 갱신한다.

M0 세션 3의 그림으로 돌아가면, 경로 길이가 1이라는 말의 뜻이 여기서 분명해진다. 백 칸 앞의 토큰이든 바로 앞의 토큰이든 가중합에 한 항으로 들어갈 뿐이다. 중간 단계가 없다.

확률로 읽는 방법

A의 한 행을 보면 그 토큰이 무엇에 주목했는지 읽을 수 있다. 이것을 시각화한 것이 attention 히트맵이다.

다만 해석에 주의가 필요하다. attention 가중치가 크다는 것이 "그 토큰이 중요하다"는 뜻은 아니다. 가중치는 그 층, 그 헤드에서의 정보 이동 비율일 뿐이고, 실제로 최종 예측에 얼마나 기여했는지는 뒤에 이어지는 층들을 거쳐야 정해진다. 어텐션 가중치를 모델의 설명으로 곧바로 쓰는 것은 위험하다는 것이 여러 연구의 결론이다.

엔트로피로 읽는 방법

M1 세션 16의 도구를 여기에 쓸 수 있다. 한 행의 엔트로피를 계산하면 그 토큰이 얼마나 넓게 주의를 퍼뜨렸는지 알 수 있다. 엔트로피가 낮으면 한두 토큰에 집중한 것이고, 높으면 전체를 고르게 본 것이다.

실제 모델을 분석해 보면 헤드마다 성격이 다르다. 어떤 헤드는 항상 바로 앞 토큰만 보고, 어떤 헤드는 전체를 고르게 보며, 어떤 헤드는 특정 문법 관계를 따라간다. 이것이 다음 세션의 multi-head가 필요한 이유로 이어진다.

확인 질문

  1. attention 가중치 행렬의 한 행이 만족하는 제약은 무엇인가?
  2. 출력 벡터가 어떤 V와도 같지 않은 이유는?
  3. attention 가중치가 높다는 것을 "그 토큰이 중요하다"로 읽으면 안 되는 이유는?
확인

1. 모든 값이 0 이상이고 합이 정확히 1이다. 행 방향으로 softmax를 적용했기 때문이다.

2. 여러 V의 가중 평균이기 때문이다. 가중치가 한 곳에 완전히 몰리지 않는 한 결과는 새로 조합된 벡터다.

3. 가중치는 그 층, 그 헤드에서의 정보 이동 비율일 뿐이고, 최종 예측에 대한 기여는 이후 층들을 거쳐 정해지기 때문이다.


Session 52multi-head는 왜 필요한가

하나의 softmax는 하나의 관계만 강조할 수 있다. 여러 개의 attention을 서로 다른 부분공간에서 병렬로 돌리면, 문법 관계와 의미 관계와 위치 관계를 동시에 볼 수 있다.

하나로는 부족한 이유

softmax는 확률분포를 만든다. 합이 1이므로 어딘가에 많이 주면 다른 곳에는 적게 준다. 즉 하나의 attention은 하나의 기준으로만 주의를 배분할 수 있다.

그런데 언어에서 한 토큰은 여러 관계에 동시에 참여한다. "그가"라는 토큰은 앞의 선행사를 참조하면서, 동시에 뒤의 동사와 주어-술어 관계를 맺고, 문장 전체의 주제와도 연결된다. 이것들을 하나의 분포로 표현하려면 확률을 나눠 가져야 하고, 결국 어느 것도 제대로 보지 못한다.

나누어 병렬로

해결책은 attention을 여러 개 돌리는 것이다. 다만 각각을 전체 차원 d로 돌리면 비용이 헤드 수만큼 곱해진다. 그래서 차원을 나눈다.

모델 차원이 d이고 헤드가 H개면 각 헤드는 dh=d/H 차원을 담당한다. 예를 들어 d=4096, H=32dh=128이다.

각 헤드는 자기 몫의 W_Q, W_K, W_V를 갖고, 자기 차원에서 독립적으로 attention을 계산한다. H개의 [T, d_h] 출력이 나오면 이것을 옆으로 이어 붙여 [T, d]로 되돌린다. 마지막으로 출력 투영 행렬 W_O를 곱한다.

MHA(X)=Concat(head1,,headH),WO

비용이 늘지 않는 이유

여기가 설계의 묘미다. 헤드를 H개 만들었는데 계산량은 거의 그대로다.

각 헤드의 점수 행렬은 [T, T]이고 헤드가 H개이므로 H×T×T다. 그런데 각 헤드의 내적은 dh=d/H 차원에서 이루어지므로, 곱셈 횟수는 H×T×T×(d/H)=T×T×d다. 헤드 수가 약분되어 사라진다. 단일 헤드로 d 차원 attention을 하는 것과 계산량이 같다.

즉 multi-head는 같은 비용으로 여러 관점을 얻는 공짜에 가까운 개선이다. 이것이 표준이 된 이유다.

출력 투영의 역할

마지막 W_O가 왜 필요한지 짚어 둔다. 헤드들의 출력을 이어 붙이면 각 구간이 독립적인 부분공간의 결과다. W_O는 이들을 섞어서 하나의 통합된 표현으로 만든다. 이것이 없으면 헤드들이 서로 소통하지 못하고 각자의 구간에 갇힌다.

파라미터를 세면 attention 블록 전체는 W_Q, W_K, W_V, W_O 네 개의 [d, d] 행렬, 즉 4d2다. 이 숫자를 기억해 두면 세션 59에서 모델 크기를 손으로 계산할 때 쓴다.

헤드가 실제로 하는 일

학습된 모델을 분석해 보면 헤드마다 뚜렷한 역할 분화가 관측된다. 바로 앞 토큰만 보는 헤드, 문장의 첫 토큰에 몰아주는 헤드, 같은 단어가 이전에 나온 자리를 찾는 헤드, 특정 문법 관계를 따라가는 헤드 등이다.

다만 모든 헤드가 유용한 것은 아니다. 상당수의 헤드를 제거해도 성능이 거의 떨어지지 않는다는 연구가 있고, 이것이 모델 압축의 한 갈래가 되었다.

확인 질문

  1. 하나의 attention이 여러 관계를 동시에 표현하기 어려운 이유는?
  2. 헤드를 H개로 늘려도 계산량이 거의 그대로인 이유를 계산해 보라.
  3. 출력 투영 W_O가 하는 일은 무엇인가?
확인

1. softmax의 출력은 합이 1인 확률분포라서, 한 곳에 많이 주면 다른 곳에는 적게 줄 수밖에 없다. 여러 관계에 동시에 큰 가중치를 줄 수 없다.

2. 헤드가 H개이므로 점수 행렬은 H배 늘지만 각 헤드의 차원이 d/H로 줄어든다. 곱하면 H×T×T×d/H=T×T×dH가 약분된다.

3. 헤드들의 출력을 섞어 하나의 통합된 표현으로 만든다. 이것이 없으면 각 헤드의 결과가 자기 부분공간에 갇혀 서로 결합되지 못한다.


Session 53tensor가 겪는 여정 전체

지금까지의 모든 단계를 텐서 모양으로 한 번에 이어 본다. 이 표를 참조 없이 재현할 수 있으면 attention은 끝난 것이고, 그러지 못하면 앞의 세션들로 돌아가야 한다.

전 구간

배치 B, 시퀀스 길이 T, 모델 차원 d, 헤드 수 H, 헤드 차원 dh=d/H로 두고 따라간다.

입력 토큰에서 attention 출력까지

[B, T] [B, T, d] [B, T, 3d] [B, H, T, d_h] [B, H, T, T] [B, H, T, T] [B, H, T, d_h] [B, T, d] [B, T, d] 토큰 ID 임베딩 조회 Q·K·V 투영 헤드로 분할, RoPE 적용 QKᵀ / √d_h 마스크 + softmax A·V 가중합 헤드 병합 출력 투영 W_O ← 여기가 T² 지점 ← 메모리 병목
주황색 두 줄이 유일하게 T2이 등장하는 구간이다. 나머지는 전부 T에 선형이다. 그래서 긴 컨텍스트를 다루는 모든 기법은 이 두 줄을 어떻게 처리할 것인가에 대한 답이다.

각 변환을 말로

기억할 규칙

이 여정에서 뽑아낼 규칙은 세 개다.

확인 질문

  1. 위 표를 보지 않고 [B, T]에서 시작해 아홉 단계의 모양을 순서대로 적어 보라.
  2. 헤드 분할이 값을 바꾸지 않는다는 것은 무슨 뜻인가?
  3. 전 구간에서 T2이 나타나는 곳은 몇 군데이며 어디인가?
확인

1. [B,T][B,T,d][B,T,3d][B,H,T,d_h][B,H,T,T][B,H,T,T][B,H,T,d_h][B,T,d][B,T,d].

2. 같은 숫자들을 다른 모양으로 재해석하고 축 순서만 바꾸는 것이다. 어떤 연산도 값에 적용되지 않는다.

3. 두 군데다. 점수 계산 QKT와 그 뒤의 마스크·softmax 구간이다.


Session 54attention의 비용 구조

Attention의 파라미터는 시퀀스 길이와 무관하지만 연산량과 메모리는 길이의 제곱으로 자란다. 이 비대칭이 긴 컨텍스트를 비싸게 만들고, 이후 모든 최적화 기법의 출발점이 된다.

세 가지를 나눠 세기

혼동을 피하려면 세 가지를 구별해야 한다.

언제 어느 쪽이 지배하는가

투영 부분은 Td2, 점수 부분은 T2d에 비례한다. 둘의 비율은 T/d다.

Td보다 작으면 투영이 지배하고, Td보다 크면 점수 계산이 지배한다. d가 4096인 모델에서 컨텍스트가 512라면 attention의 제곱 비용은 별로 중요하지 않다. 컨텍스트가 32000이라면 압도적으로 지배한다.

이 관찰은 실무에서 유용하다. 짧은 프롬프트를 다루는 서비스에서는 attention 최적화가 큰 이득이 없고, 긴 문서를 다루는 RAG 시스템에서는 결정적이다.

길이에 따라 무엇이 비용을 지배하는가

4k
모델 차원 4096, 헤드 32, 배치 1 기준이다. 길이가 d를 넘어서는 순간부터 제곱 항이 선형 항을 앞지른다. 그리고 점수 행렬의 메모리는 더 빨리 자라서, 최적화 없이는 긴 컨텍스트가 아예 불가능해진다.

어떻게 대응하는가

크게 네 갈래가 있고, 이후 모듈에서 각각 다룬다.

확인 질문

  1. attention의 파라미터 수가 시퀀스 길이와 무관한 이유는?
  2. 투영 비용과 점수 비용 중 어느 쪽이 지배하는지는 무엇으로 판단하는가?
  3. 점수 행렬의 메모리가 특히 문제가 되는 이유를 배치와 헤드를 포함해 설명해 보라.
확인

1. 학습되는 것은 W_Q, W_K, W_V, W_O 네 개의 [d, d] 행렬뿐이고 이 크기에 T가 들어가지 않기 때문이다. 길이는 입력의 성질이지 파라미터의 성질이 아니다.

2. Td의 대소다. 투영은 Td2, 점수는 T2d에 비례하므로 비율이 T/d다. 길이가 모델 차원보다 크면 점수 계산이 지배한다.

3. 점수 행렬이 [B, H, T, T]이므로 배치와 헤드 수가 곱해진다. 헤드가 32개, 배치가 8이면 T2에 256이 곱해져, 길이 8000에서도 수십 GB가 된다.


Session 55FFN은 무엇을 하는가

Attention이 토큰들 사이에 정보를 옮긴다면, FFN은 각 토큰의 정보를 혼자서 가공한다. 파라미터의 3분의 2가 여기 있고, 모델이 아는 사실의 상당 부분이 여기 저장된다는 관점이 유력하다.

구조

FFN은 두 개의 선형 계층 사이에 활성함수를 끼운 것이다.

FFN(x)=W2,σ(W1x)

W_1은 차원을 d에서 d_ff로 늘리고, W_2는 다시 d로 줄인다. 관례적으로 d_ff = 4d를 쓴다. d가 4096이면 중간 차원이 16384다.

중요한 것은 이 연산이 토큰마다 독립적으로 적용된다는 점이다. [B, T, d] 텐서의 각 위치에 같은 함수를 따로 적용한다. 다른 토큰을 전혀 보지 않는다.

역할 분담

여기서 Transformer 블록의 설계 의도가 드러난다.

둘이 번갈아 반복되면서 정보가 퍼지고 가공되기를 반복한다. 이 역할 분담이 Transformer 구조의 핵심이다.

왜 4배로 늘리는가

중간에서 차원을 늘렸다 줄이는 이유는 표현력이다. 차원을 늘린 공간에서 비선형 변환을 적용하면, 원래 차원에서는 표현할 수 없는 함수를 만들 수 있다. 좁은 공간에서 바로 변환하는 것보다 넓혔다가 다시 좁히는 편이 훨씬 강력하다.

4배라는 숫자는 실험적으로 정해진 관례이고 절대적인 것은 아니다. 다만 널리 쓰여 왔고, 대부분의 모델이 이 근처를 유지한다.

SwiGLU와 파라미터 계산

M2 세션 23에서 언급한 대로 최근 모델은 SwiGLU를 쓴다. 입력을 두 갈래로 나눠 한쪽을 게이트로 쓰는 구조라 가중치 행렬이 세 개다.

SwiGLU(x)=W3big(Swish(W1x)W2xbig)

이 때문에 파라미터 계산에서 실수하기 쉽다. 일반 FFN이 2 × d × d_ff인 반면 SwiGLU는 3 × d × d_ff다. 그래서 총 파라미터를 맞추려고 d_ff4d가 아니라 8d/3 정도로 줄여 쓰는 관행이 생겼다. 8/3×3=8=4×2가 되어 일반 FFN과 파라미터가 같아진다.

실제 모델의 설정을 보면 d_ff4d가 아닌 어정쩡한 값인 경우가 많은데, 대개 이 계산의 결과다.

파라미터 비중

일반 FFN 기준으로 블록 하나의 파라미터를 세어 보자.

attention=4d2,qquadFFN=2×d×4d=8d2

합이 12d2이고 그중 FFN이 8d2, 즉 3분의 2다. 이 비율은 M7과 M8에서 계속 쓰인다. 예를 들어 추론 시 어떤 연산이 시간을 많이 쓰는지, 양자화로 어디를 줄여야 효과가 큰지가 이 비율에서 나온다.

지식이 저장되는 곳이라는 관점

FFN을 키-값 메모리로 보는 해석이 있다. W_1의 각 행이 어떤 패턴을 감지하는 키이고, 그 패턴이 감지되면 W_2의 대응하는 열이 값으로 더해진다는 것이다. 실제로 특정 뉴런이 특정 사실이나 개념에 반응하는 현상이 관측되고, 그 뉴런을 조작해 모델의 지식을 편집하려는 연구도 있다.

이 관점이 실무에 주는 함의는 이렇다. 모델이 아는 사실은 주로 FFN의 가중치에 분산되어 있으므로, 지식을 바꾸려면 그 가중치를 바꿔야 한다. 그런데 그것은 어렵고 부작용이 크다. 그래서 M10의 RAG처럼 지식을 파라미터 밖에 두는 접근이 현실적인 답이 된다.

확인 질문

  1. FFN이 attention과 역할이 어떻게 다른가?
  2. 모델 차원이 4096이고 일반 FFN을 쓸 때 블록 하나의 파라미터를 계산해 보라.
  3. SwiGLU를 쓸 때 d_ff8d/3으로 잡는 이유는?
확인

1. attention은 토큰들 사이에 정보를 옮기고, FFN은 각 토큰의 정보를 다른 토큰을 보지 않고 혼자서 비선형 변환한다. 섞기와 가공의 분담이다.

2. attention이 4×4096²6710, FFN이 8×4096²13400, 합쳐서 약 2억 개다.

3. SwiGLU는 게이트용 행렬이 추가되어 가중치가 세 개다. 3×d×8d/3=8d2가 되어 일반 FFN의 2×d×4d=8d2와 파라미터 수가 같아진다.


Session 56residual stream 관점

블록들이 값을 덮어쓰지 않고 더하기만 한다는 사실에서 강력한 해석이 나온다. 잔차 경로를 층들이 공유하는 하나의 버스로 보면, Transformer의 동작이 훨씬 단순하게 읽힌다.

덧셈만 한다

M2 세션 31에서 본 잔차 연결이 Transformer의 모든 서브층에 붙어 있다. 블록 하나는 이렇게 생겼다.

xx+Attention(Norm(x))
xx+FFN(Norm(x))

두 줄 모두 x에 무언가를 더할x를 대체하지 않는다. 32층 모델이라면 이런 덧셈이 64번 일어나고, 마지막 x는 초기 임베딩에 64개의 기여가 누적된 결과다.

xfinal=xembed+l=1Lbig(Attnl+FFNlbig)

버스라는 그림

이 구조를 읽는 방법이 residual stream 관점이다. x를 층들이 공유하는 작업 공간으로 본다.

각 서브층은 이 공간에서 현재 내용을 읽고(정규화를 거쳐 Q, K, V나 FFN 입력으로), 무언가를 계산해서, 결과를 공간에 쓴다(더한다). 지우지 않으므로 이전 층이 남긴 내용은 계속 살아 있고, 이후 모든 층이 그것을 볼 수 있다.

이 그림이 유용한 이유는 몇 가지 현상을 자연스럽게 설명하기 때문이다.

정규화의 위치가 뜻하는 것

이 관점에서 pre-LN의 의미가 분명해진다. 정규화가 서브층의 입력에만 걸리고 버스 자체에는 걸리지 않는다. 즉 각 층은 버스를 읽을 때 정규화된 사본을 보지만, 버스에 쓸 때는 원래 스케일로 더한다. 버스 자체는 한 번도 정규화되지 않은 채 층을 통과한다.

이것이 gradient가 배율 1로 흐르는 통로가 되는 이유이고, 다음 세션의 주제다.

백엔드의 언어로

residual stream은 여러 처리기가 공유하는 append-only 메시지 버스다. 각 처리기가 버스를 구독해 읽고, 처리 결과를 버스에 발행한다. 이전 메시지를 삭제하지 않으므로 순서에 의존하지 않는 협업이 가능하고, 어떤 처리기가 아무것도 발행하지 않아도 시스템이 정상 동작한다.

확인 질문

  1. 32층 모델에서 잔차 스트림에 더해지는 기여는 몇 개인가?
  2. residual stream 관점이 "어떤 층은 거의 일하지 않는다"를 어떻게 설명하는가?
  3. pre-LN에서 정규화는 무엇에 걸리고 무엇에는 걸리지 않는가?
확인

1. 층마다 attention과 FFN 두 번씩이므로 64개다. 여기에 초기 임베딩이 더해진다.

2. 각 서브층이 버스에 더하기만 하므로 0에 가까운 값을 더하면 아무 영향도 주지 않는다. 즉 "일하지 않기"가 구조적으로 가능한 선택지다.

3. 서브층의 입력에만 걸린다. 잔차 스트림 자체는 정규화되지 않은 채로 층을 통과하며, 서브층의 출력은 원래 스케일로 더해진다.


Session 57pre-LN vs post-LN

정규화를 잔차 덧셈 앞에 두느냐 뒤에 두느냐가 학습 안정성을 가른다. pre-LN은 gradient가 배율 1로 흐르는 통로를 남겨 두고, post-LN은 그 통로를 막는다.

두 배치

원래 Transformer 논문은 이렇게 썼다.

xNormbig(x+Sublayer(x)big)qquad(post-LN)

현재 표준은 이렇다.

xx+Sublayerbig(Norm(x)big)qquad(pre-LN)

차이는 정규화가 덧셈의 안에 있느냐 밖에 있느냐뿐이다. 그런데 결과가 크게 다르다.

통로가 있느냐

pre-LN에서 x는 아무 변형 없이 다음 층으로 넘어간다. 역전파에서 이 경로의 미분은 정확히 1이다. M2 세션 31에서 본 대로, 층이 아무리 깊어도 이 통로를 따라 gradient가 감쇠 없이 흐른다.

post-LN에서는 x가 정규화를 통과한다. 정규화는 값의 스케일을 바꾸는 연산이고 미분도 1이 아니다. 층마다 이 배율이 곱해지므로, 층이 깊어질수록 앞쪽에 도달하는 gradient가 나빠진다.

관측되는 결과

post-LN 모델은 학습 초기에 불안정하다. warmup 없이 시작하면 손실이 발산하는 경우가 많고, warmup 구간을 길게 잡아야 한다. 층을 깊게 쌓을수록 이 문제가 심해져서, 원래 논문의 6층 규모에서는 다룰 만했지만 수십 층 규모에서는 감당이 어려웠다.

pre-LN은 warmup 없이도 비교적 안정적이고 깊이에 훨씬 관대하다. 그래서 오늘날의 대형 모델은 거의 전부 pre-LN이다.

대가

공짜는 아니다. pre-LN에는 알려진 부작용이 있다.

잔차 스트림이 한 번도 정규화되지 않으므로 층을 지날수록 값의 크기가 계속 커진다. 각 층이 계속 더하기만 하기 때문이다. 층이 깊어지면 뒤쪽 층의 입력 스케일이 앞쪽과 크게 달라지고, 이 때문에 뒤쪽 층의 기여가 상대적으로 작아진다는 분석이 있다.

그래서 대부분의 구현은 모든 블록을 통과한 뒤 마지막에 정규화를 한 번 더 넣는다. 이것을 final norm이라고 부르고, 출력층으로 넘기기 전에 스케일을 정리하는 역할을 한다. 모델 구조도를 볼 때 블록 밖에 정규화가 하나 더 있으면 이것이다.

확인 질문

  1. pre-LN과 post-LN의 식을 각각 써 보라.
  2. pre-LN에서 gradient가 감쇠 없이 흐르는 경로는 무엇인가?
  3. pre-LN의 부작용과 그에 대한 대응은?
확인

1. post-LN은 xNorm(x+Sublayer(x)), pre-LN은 xx+Sublayer(Norm(x))다.

2. 입력 x가 아무 변형 없이 출력의 덧셈 항으로 들어가는 경로다. 덧셈의 미분은 1이므로 이 통로에서는 감쇠가 일어나지 않는다.

3. 잔차 스트림이 정규화되지 않아 층을 지날수록 값이 계속 커진다. 대응으로 모든 블록을 통과한 뒤 마지막에 정규화를 한 번 더 적용한다.


Session 58Transformer 블록 조감

여기서 부품이 하나로 조립된다. 지금까지 배운 정규화, attention, 잔차, FFN이 두 줄의 식으로 정리되고, 이 블록을 수십 번 쌓은 것이 모델의 전부다.

두 줄

xx+Attentionbig(RMSNorm(x)big)
xx+FFNbig(RMSNorm(x)big)

M2 세션 34의 마지막에서 예고한 그 식이다. 그때는 AttentionFFN이 빈칸이었고, 지금은 채워져 있다.

블록 하나의 내부

residual stream RMSNorm Multi-Head Attention RMSNorm FFN (SwiGLU) W_Q W_K W_V W_O · 4d² W₁ W₂ W₃ · 8d² + + 읽기 → 계산 → 더하기 토큰 사이를 섞는다 각 토큰을 혼자 가공한다
왼쪽 굵은 선이 잔차 스트림이다. 두 서브층 모두 정규화된 사본을 읽고, 계산하고, 원래 스케일로 더한다. 스트림 자체는 어떤 변형도 받지 않고 블록을 통과한다. 이 그림을 백지에서 그릴 수 있으면 M4의 절반은 끝난 것이다.

순서와 이유

블록 안의 순서에는 이유가 있다.

블록을 쌓는다는 것

이 블록을 L번 반복한 것이 모델의 몸통이다. 각 블록은 자기만의 파라미터를 갖는다. 같은 함수를 반복 적용하는 것이 아니라 서로 다른 L개의 함수를 순서대로 적용하는 것이다.

층 수와 모델 차원 사이에는 경험적인 균형이 있다. 너무 깊고 좁으면 학습이 어렵고, 너무 얕고 넓으면 표현력이 떨어진다. 실제 모델들의 설정을 보면 대체로 d가 층 수의 100배 안팎이다. 예를 들어 32층에 4096차원, 80층에 8192차원 같은 조합이다.

확인 질문

  1. Transformer 블록의 두 줄을 참조 없이 써 보라.
  2. 두 서브층이 잔차 연결을 각각 따로 갖는 이유는?
  3. 블록을 32번 쌓았을 때, 32개의 블록은 같은 파라미터를 공유하는가?
확인

1. xx+Attention(Norm(x)), 그다음 xx+FFN(Norm(x)).

2. gradient가 흐르는 통로를 두 개 확보하고, 각 서브층이 독립적으로 0을 기여하는 선택을 할 수 있게 하기 위해서다.

3. 아니다. 각 블록이 자기만의 파라미터를 갖는다. 같은 함수를 반복하는 것이 아니라 서로 다른 32개의 변환을 순서대로 적용한다.

앉아서 읽을 것

이제 Attention Is All You Need의 3절을 읽을 준비가 됐다. 여기까지 온 상태에서 읽으면 새로 배울 것은 거의 없고, 대신 자신이 이해한 것이 원문과 일치하는지 확인하는 경험이 된다. 그 확인이 이 시점에서 가장 값진 것이다. The Illustrated Transformer를 함께 보면 그림으로 한 번 더 굳는다.


Session 59GPT 전체 구조와 파라미터 세기

임베딩, 블록 L개, 마지막 정규화, 출력층. 이것이 전부다. 그리고 설정 파일만 보고 파라미터 수를 손으로 계산할 수 있어야 이후 모듈의 메모리 계산이 가능해진다.

전체 구조

토큰 ID [B, T]가 들어온다. 임베딩 테이블을 조회해 [B, T, d]가 된다. Transformer 블록 L개를 차례로 통과한다. 모양은 계속 [B, T, d]다. 마지막 정규화를 거친 뒤 출력층 [d, V]를 곱해 [B, T, V]가 된다. 각 위치에서 어휘 전체에 대한 점수다.

M0 세션 1의 그림이 여기서 완성된다. 학습에서는 이 [B, T, V] 전체를 쓰고 각 위치의 손실을 평균한다. 생성에서는 마지막 위치의 [B, V]만 쓴다.

파라미터 세기

이제 실제로 세어 보자. 세 부분으로 나뉜다.

P12Ld2+2Vd

실제로 해 보기

8B급 모델의 전형적인 설정으로 계산해 보자. L=32, d=4096, V=128000, SwiGLU에 d_ff = 14336, GQA로 n_kv_heads = 8, n_heads = 32라고 하자.

부분계산파라미터
임베딩128000×40965.24억
출력층128000×40965.24억
attention (블록당)GQA라 Q·O는 d2씩, K·V는 d×d/44194만
FFN (블록당)3×4096×143361.76억
블록 32개(0.42+1.76)×3269.8억
합계약 80억

이 계산에서 몇 가지가 눈에 들어온다.

왜 세어 보는가

이 계산이 실무에서 쓰이는 자리는 명확하다.

M8 세션 107에서 이 계산을 VRAM 산정으로 확장한다.

확인 질문

  1. 블록 하나의 파라미터를 d로 표현하면 대략 얼마인가? 그중 FFN 비중은?
  2. 모델 차원을 2배로 늘리는 것과 층 수를 2배로 늘리는 것 중 어느 쪽이 파라미터를 더 늘리는가?
  3. L=24, d=2048, V=32000인 모델의 파라미터를 대략 계산해 보라.
확인

1.12d2다. attention이 4d2, FFN이 8d2이므로 FFN이 3분의 2다. GQA를 쓰면 attention이 더 줄어 FFN 비중이 더 커진다.

2. 차원이다. 파라미터가 d2에 비례하므로 차원을 2배로 하면 4배가 되지만, 층 수는 선형이라 2배만 된다.

3. 블록이 12×24×2048²12.1, 어휘가 2×32000×20481.31이므로 합쳐서 약 13.4억, 즉 1.3B 정도다.


Session 60MQA와 GQA

헤드마다 K와 V를 따로 두지 않고 여러 헤드가 공유하면 KV cache 크기가 그만큼 줄어든다. 품질을 위한 설계가 아니라 추론 메모리를 위한 설계이고, 이 사실을 알면 왜 최근 모델이 전부 이렇게 생겼는지 이해된다.

문제

M7에서 자세히 보겠지만, 생성할 때는 이전 토큰들의 K와 V를 저장해 두고 재사용한다. 그 크기는 이렇게 계산된다.

KVcache=2×L×H×dh×T×B×bytes

헤드 수 H가 곱해져 있다. 32층에 32헤드, 헤드 차원 128, bf16이라면 토큰 하나당 2×32×32×128×2=524288 바이트, 즉 512 KiB다. 컨텍스트 8000에 동시 요청 16개면 8000×16×512KiB=64GiB다. 모델 가중치보다 크다.

이것이 긴 컨텍스트와 높은 동시성을 동시에 달성하기 어려운 근본 이유다.

공유하기

관찰은 이렇다. 헤드마다 Q는 달라야 한다. 각 헤드가 서로 다른 것을 찾아야 하니까. 그런데 K와 V까지 전부 달라야 할까.

Q는 헤드마다, K와 V는 그룹마다

MHA — KV 8쌍 GQA — KV 2쌍 (4분의 1) MQA — KV 1쌍 (8분의 1)
위 칸은 Q 헤드로 세 방식 모두 8개다. 아래 칸이 K·V이고, 선은 어느 Q가 어느 KV를 쓰는지 보여 준다. 줄어드는 것은 KV cache와 그것을 읽는 대역폭이지 Q의 표현력이 아니다. M7의 메모리 공식에서 H 자리에 들어가는 값이 바로 이 아래 칸의 개수다.

무엇이 줄고 무엇이 그대로인가

즉 GQA는 추론 효율을 위해 표현력을 조금 내주는 거래이고, 그 비율이 매우 유리해서 표준이 되었다.

설정에서 읽는 법

모델 설정 파일에 num_attention_headsnum_key_value_heads가 따로 있으면 GQA를 쓰는 것이다. 두 값이 같으면 일반 MHA, num_key_value_heads가 1이면 MQA다.

이 값이 KV cache 계산에 직접 들어가므로, 서빙 용량을 산정할 때 반드시 확인해야 한다. num_attention_heads로 계산하면 실제보다 몇 배 크게 나온다.

확인 질문

  1. GQA가 줄이는 것과 유지하는 것을 각각 말해 보라.
  2. 32헤드를 8그룹으로 묶으면 KV cache가 몇 배로 줄어드는가?
  3. KV cache 계산에서 헤드 수 자리에 어떤 값을 넣어야 하는가?
확인

1. 줄이는 것은 KV cache 메모리와 생성 시 그것을 읽는 대역폭이다. 유지하는 것은 Q 헤드 수, 즉 여러 관점으로 보는 능력과 대략적인 계산량이다.

2. 4분의 1이다. K와 V가 헤드마다 8개가 아니라 그룹마다 하나씩 총 8개가 되므로 32에서 8로 줄어든다.

3. num_key_value_heads다. num_attention_heads를 쓰면 GQA 모델에서 몇 배 과대 계산된다.


Session 61MoE 개요

MoE는 FFN을 여러 개 두고 토큰마다 그중 일부만 쓴다. 파라미터는 크게 늘리면서 토큰당 계산량은 유지하는 구조이고, 대신 서빙이 어려워진다.

발상

M5에서 볼 scaling law는 파라미터를 늘리면 성능이 좋아진다고 말한다. 그런데 파라미터를 늘리면 계산량도 함께 늘어난다. 이 둘을 떼어 놓을 수 있을까.

MoE의 답은 이렇다. FFN을 하나가 아니라 여러 개 준비한다. 이것을 전문가라고 부른다. 그리고 토큰마다 라우터가 몇 개의 전문가만 골라 그것만 실행한다. 전문가가 64개인데 토큰마다 2개만 쓴다면, 파라미터는 32배지만 토큰당 계산량은 그대로다.

구조

FFN 자리에 두 가지가 들어간다. 작은 라우터 네트워크와 N개의 전문가 FFN이다.

라우터는 토큰 벡터를 받아 각 전문가에 대한 점수를 내고, 상위 k개를 고른다. 보통 k는 1이나 2다. 선택된 전문가들의 출력을 라우터 점수로 가중합해 최종 출력을 만든다.

여기서 문제가 하나 생긴다. 라우터가 특정 전문가만 계속 고르면 나머지는 학습되지 않는다. 그래서 부하를 고르게 분산시키는 보조 손실을 추가한다. 이 균형 잡기가 MoE 학습의 까다로운 부분이다.

두 개의 파라미터 수

MoE 모델을 볼 때는 숫자가 두 개다.

예를 들어 "총 141B, 활성 39B" 같은 표기를 보게 되는데, 메모리는 141B 기준으로 준비하되 속도는 39B 모델에 가깝다는 뜻이다.

서빙이 어려운 이유

이 구조가 추론에 주는 부담이 실무적으로 중요하다.

그래서 MoE 모델은 "같은 활성 파라미터의 조밀 모델보다 빠르다"는 이론과 "실제 서빙에서는 그만큼 안 나온다"는 현실 사이에 간극이 있다. M9에서 엔진을 고를 때 MoE 지원 여부와 성숙도를 따로 확인해야 하는 이유다.

확인 질문

  1. MoE가 파라미터와 계산량을 분리하는 원리를 설명해 보라.
  2. "총 141B, 활성 39B"라는 표기에서 메모리와 속도는 각각 어느 쪽을 따르는가?
  3. MoE 서빙이 조밀 모델보다 어려운 이유를 두 가지 말해 보라.
확인

1. FFN을 여러 개 두되 토큰마다 그중 일부만 실행한다. 파라미터 총량은 전문가 수만큼 늘지만 토큰당 실행되는 것은 선택된 몇 개뿐이라 계산량은 유지된다.

2. 메모리는 총 141B를 따르고 속도는 활성 39B에 가깝다. 어떤 전문가가 불릴지 모르므로 전부 올려 둬야 한다.

3. 첫째, 배치 안 토큰들이 서로 다른 전문가를 불러 부하가 불균형해지고 GPU 효율이 떨어진다. 둘째, 전문가를 여러 GPU에 나누면 토큰을 보내고 받는 통신이 새 병목이 된다.


Session 62복습 — 이 트랙의 척추

이 세션이 관문 G2다. attention을 수식과 텐서 모양으로 5분간 말로 설명할 수 있어야 다음으로 넘어간다. 여기서 막히면 진도를 멈추고 M4를 다시 보는 것이 옳다.

한 번에 이어 말하기

먼저 전체를 하나의 흐름으로 재구성해 보자. 아래를 읽지 말고 스스로 말해 본 뒤에 대조하는 편이 낫다.

토큰 배열이 임베딩을 거쳐 [B, T, d]가 되어 블록으로 들어온다. 블록은 정규화된 사본을 읽어 세 개의 행렬로 Q, K, V를 만든다. 세 개가 필요한 이유는 "무엇을 찾는가", "무엇을 제공하는가", "무엇을 전달하는가"가 서로 다른 정보이기 때문이다.

이들을 헤드로 나눈다. [B, T, d][B, H, T, d_h]가 되는데, 값은 그대로이고 보는 방식만 바뀐다. 헤드가 여럿인 이유는 하나의 softmax가 하나의 관계만 강조할 수 있기 때문이고, 차원을 나눠 쓰므로 계산량은 늘지 않는다. 이 시점에 RoPE가 Q와 K에 적용되어 위치 정보가 들어간다.

Q와 K를 곱해 [B, H, T, T] 점수 행렬을 만든다. 여기가 유일한 제곱 지점이다. 이 값을 dh로 나누는데, 내적의 분산이 차원에 비례해 커져서 그대로 두면 softmax가 포화되고 기울기가 사라지기 때문이다.

미래 위치에 -∞를 더해 마스킹한다. softmax 이전에 더해야 남은 확률의 합이 1이 된다. 행 방향 softmax를 적용하면 각 토큰이 다른 토큰에 배분한 주의의 비율이 나온다.

이 비율로 V를 가중 평균한다. 결과가 [B, H, T, d_h]이고, 헤드를 다시 이어 붙여 [B, T, d]로 만든 뒤 출력 투영을 곱한다. 그리고 이 결과를 잔차 스트림에 더한다.

정규화된 사본을 다시 읽어 FFN을 통과시킨다. FFN은 다른 토큰을 보지 않고 각 위치를 독립적으로 가공하며, 블록 파라미터의 3분의 2가 여기 있다. 결과를 다시 스트림에 더한다.

이 블록을 L번 반복하고, 마지막 정규화를 거쳐 출력층을 통과하면 [B, T, V], 즉 각 위치의 다음 토큰 분포가 나온다.

관문 G2

  1. Q, K, V가 각각 무엇이고 왜 이어야 하는가? K와 V를 합치면 무엇이 안 되는가? (S47)
  2. [B, T]에서 attention 출력까지 아홉 단계의 텐서 모양을 순서대로 말해 보라. (S53)
  3. d로 나누는 이유를 분산 → 포화 → 기울기의 세 단계로 설명해 보라. 여기서 d는 무엇인가? (S49)
  4. 마스킹을 softmax 이전에 더하는 이유는? 이후에 0을 곱하면 무엇이 깨지는가? (S50)
  5. multi-head가 필요한 이유와, 헤드를 늘려도 계산량이 늘지 않는 이유를 각각 말해 보라. (S52)
  6. attention에서 T2이 나타나는 곳은 어디이며, 파라미터는 왜 T와 무관한가? (S54)
  7. attention과 FFN의 역할 분담을 한 문장씩으로 말하고, 파라미터 비중을 대라. (S55)
  8. pre-LN이 표준이 된 이유를 잔차 경로의 미분으로 설명해 보라. (S57)
  9. L=32,d=4096,V=128000인 모델의 파라미터를 대략 계산해 보라. (S59)
  10. GQA가 줄이는 것은 무엇이고, 그것이 왜 중요한가? (S60)
확인

1. Q는 찾는 것, K는 제공하는 것, V는 전달할 내용이다. K와 V를 합치면 "무엇을 기준으로 선택될 것인가"와 "선택되면 무엇을 줄 것인가"를 하나의 벡터에 담아야 해서 서로 방해한다. 둘은 성격이 다른 정보다.

2. [B,T][B,T,d][B,T,3d][B,H,T,d_h][B,H,T,T][B,H,T,T][B,H,T,d_h][B,T,d][B,T,d].

3. 분산 1인 항 d개를 더하므로 내적의 분산이 d, 표준편차가 d가 된다. 이 진폭이 그대로 들어가면 softmax가 한 곳으로 포화된다. 포화된 지점에서는 기울기가 0에 가까워 학습 신호가 사라진다. 여기서 d는 헤드 차원 d_h다.

4. softmax 이전에 -∞를 더하면 exp가 0이 되어 그 항이 제외된 채 나머지가 정규화된다. 이후에 0을 곱하면 미래 토큰이 이미 가져간 확률이 사라져 남은 확률의 합이 1보다 작아진다.

5. softmax 출력은 합이 1인 분포라 한 기준으로만 주의를 배분할 수 있는데, 언어에서는 한 토큰이 여러 관계에 동시에 참여한다. 헤드가 H개면 점수 행렬이 H배 늘지만 각 헤드 차원이 d/H로 줄어 H가 약분된다.

6. 점수 계산 QKT와 그 뒤 마스크·softmax 구간이다. 파라미터는 W_Q, W_K, W_V, W_O 네 개의 [d,d] 행렬뿐이고 여기에 T가 들어가지 않는다.

7. attention은 토큰들 사이에 정보를 옮기고, FFN은 각 토큰을 독립적으로 비선형 가공한다. 블록 파라미터 12d2 중 FFN이 8d2로 3분의 2다.

8. pre-LN에서는 입력이 아무 변형 없이 출력에 더해지는 경로가 있어 그 경로의 미분이 정확히 1이다. 층이 깊어져도 gradient가 감쇠 없이 앞쪽까지 도달한다. post-LN은 잔차를 더한 뒤 정규화가 걸려 이 통로가 막힌다.

9. 블록이 12×32×4096²64.4, 어휘가 2×128000×409610.5이므로 합쳐 약 75억, 대략 8B다.

10. KV cache 메모리와 생성 시 그것을 읽는 대역폭을 줄인다. 생성 단계는 메모리 대역폭에 묶여 있고 KV cache가 모델 가중치보다 커질 수 있으므로, 이것이 긴 컨텍스트와 높은 동시성을 동시에 달성하는 열쇠가 된다.

통과하지 못했다면

열 문항 중 두 개 이상에서 막혔다면 다음 모듈로 넘어가지 않는 것이 낫다. 특히 2번(텐서 모양)과 3번(d)에서 막혔다면 반드시 돌아가야 한다. 이후 M7의 KV cache, M8의 FlashAttention, M9의 배칭은 전부 이 두 가지 위에 세워진다.

돌아갈 때는 읽는 것이 아니라 말하는 연습을 해야 한다. 세션을 다시 읽으면 이해한 것 같은 느낌이 들지만, 그 느낌과 설명할 수 있는 상태는 다르다. 백지에 텐서 모양 표를 그려 보고, 소리 내어 한 번 설명해 보는 것이 훨씬 효과적이다.

모듈을 마치며

여기까지 오면 모델의 내부는 다 본 것이다. 남은 것은 이 모델을 어떻게 만들고(M5, M6) 어떻게 돌리고(M7, M8, M9) 무엇에 쓰는가(M10, M11, M12)다. 그리고 이후 모든 모듈이 이 모듈에서 세운 그림 위에서 이야기된다.

다음 모듈은 사전학습이다. 이 구조에 인터넷 규모의 텍스트를 흘려보내면 무슨 일이 일어나는지, 그리고 그 비용이 어떻게 계산되는지를 열 번의 이동에 걸쳐 본다.

M5세션 63–7210개

사전학습

직접 할 일은 없지만 모델이 무엇을 알고 무엇을 모르는지가 여기서 정해진다. 데이터·규모·비용의 삼각관계를 이해하면 "파인튜닝을 해야 하나"라는 질문에 답할 수 있게 된다.

Session 63사전학습은 무엇을 하는 일인가

인터넷 규모의 텍스트를 M4에서 본 구조에 흘려보내며 다음 토큰 예측 하나만 최적화한다. 목적함수는 단순한데 그 결과로 언어와 세계에 대한 일반적인 능력이 생긴다.

절차 자체는 단순하다

M0 세션 1과 M2 세션 34에서 본 것이 전부다. 토큰 배열을 잘라 배치를 만들고, 순전파로 각 위치의 다음 토큰 분포를 얻고, cross-entropy 손실을 계산하고, 역전파로 gradient를 얻고, AdamW로 파라미터를 갱신한다. 이것을 수십만 스텝 반복한다.

사전학습이 특별한 것은 알고리즘이 아니라 규모다. 수조 개의 토큰, 수천 장의 GPU, 수 주에서 수 개월. 그리고 그 규모에서만 나타나는 공학적 문제들이 있다.

규모가 만드는 문제들

왜 배우는가

직접 사전학습을 할 일은 거의 없다. 그런데도 이 모듈이 필요한 이유는 세 가지다.

확인 질문

  1. 사전학습의 목적함수는 파인튜닝과 다른가?
  2. 대규모 사전학습에서 데이터를 1~2 에폭만 도는 이유는?
  3. 사전학습을 직접 하지 않을 사람이 이것을 배워야 하는 이유 하나를 말해 보라.
확인

1. 같다. 둘 다 다음 토큰 예측의 cross-entropy다. 다른 것은 데이터와, 손실을 어느 토큰에서 계산하느냐다.

2. 데이터가 워낙 많아 여러 번 볼 필요도 여유도 없기 때문이다. 같은 데이터를 반복하면 암기가 시작되고 계산 예산도 낭비된다.

3. 모델이 무엇을 알고 무엇을 모르는지가 여기서 정해지므로, 문제를 RAG로 풀지 파인튜닝으로 풀지 판단하는 근거가 된다. (비용 감각, 모델 카드 해석 능력도 정답)


Session 64학습 데이터는 어떻게 만들어지는가

웹 크롤에서 시작해 언어 판별, 품질 필터, 중복 제거, 도메인 믹싱을 거친다. 이 파이프라인의 설계가 모델 성능에 미치는 영향은 아키텍처 개선보다 크다는 것이 지금의 공통된 이해다.

원천

가장 큰 덩어리는 웹 크롤이다. 공개 크롤 데이터가 수 페타바이트 규모로 존재하고, 대부분의 모델이 여기서 출발한다. 여기에 책, 논문, 코드 저장소, 위키, 포럼 등이 더해진다.

문제는 웹 크롤의 대부분이 쓸모없다는 점이다. 광고, 자동 생성 스팸, 메뉴와 푸터의 반복, 깨진 인코딩, 내용 없는 페이지가 압도적으로 많다. 그래서 걸러 내는 작업이 파이프라인의 본체가 된다.

필터링 단계

도메인 믹싱

필터를 통과한 데이터를 그냥 다 넣지 않는다. 웹, 코드, 책, 논문 등의 비율을 정한다.

이 비율이 모델의 성격을 크게 바꾼다. 코드 비중을 높이면 코딩 능력뿐 아니라 논리적 추론 능력도 함께 오른다는 관측이 여러 번 보고되었다. 학술 문헌 비중을 높이면 전문 지식이 늘지만 일상 대화가 어색해질 수 있다.

그리고 좋은 데이터는 여러 번 반복하기도 한다. 위키처럼 품질이 높은 소스는 두세 번 넣고, 웹 크롤은 한 번만 넣는 식이다. 다만 반복은 암기 위험을 키우므로 제한적으로 한다.

실무에 주는 함의

이 세션의 내용은 직접 쓸 일이 없어 보이지만, 두 가지 판단에 쓰인다.

확인 질문

  1. 웹 크롤 데이터를 그대로 쓰지 못하는 이유는?
  2. 벤치마크 오염이란 무엇이며 왜 문제인가?
  3. 도메인 믹싱 비율이 모델 성격을 바꾼 예를 하나 들어 보라.
확인

1. 대부분이 광고, 스팸, 반복 메뉴, 깨진 텍스트 등 쓸모없는 내용이기 때문이다. 걸러 내지 않으면 학습 예산 대부분이 낭비된다.

2. 평가에 쓸 벤치마크 문항이 학습 데이터에 섞여 들어간 것이다. 모델이 답을 외웠을 수 있으므로 점수가 실제 능력을 반영하지 않는다.

3. 코드 비중을 높이면 코딩 능력뿐 아니라 논리적 추론 능력도 함께 향상된다는 관측이 반복적으로 보고되었다.


Session 65중복 제거가 중요한 이유

같은 내용이 여러 번 학습되면 모델이 그것을 일반화하는 대신 외운다. 그리고 웹 크롤에는 중복이 상상 이상으로 많아서, 중복 제거만으로 성능이 눈에 띄게 오른다.

얼마나 중복되어 있는가

웹 크롤에서 같은 문서가 여러 URL에 존재하는 일은 흔하다. 미러 사이트, 재게시, 인용, 자동 번역본, 템플릿으로 생성된 페이지가 모두 중복을 만든다. 문서 전체가 같지 않더라도 문단 단위로 겹치는 경우는 훨씬 많다.

연구에 따르면 대규모 웹 코퍼스에서 중복을 제거하면 데이터 양이 수십 퍼센트 줄어든다. 그리고 그렇게 줄인 데이터로 학습한 모델이 원본으로 학습한 모델보다 더 좋다. 데이터를 버렸는데 성능이 오른 것이다.

왜 해로운가

어떻게 하는가

정확히 같은 문서를 찾는 것은 해시로 쉽다. 문제는 거의 같은 문서다. 한 글자만 달라도 해시가 완전히 달라지므로 정확 매칭으로는 잡히지 않는다.

여기서 쓰이는 것이 지역 민감 해시라는 계열의 기법이다. 아이디어는 이렇다. 문서를 겹치는 조각들로 나눈 뒤 각 조각을 해시하고, 그중 가장 작은 값 몇 개만 남긴다. 두 문서가 많이 겹치면 이 최솟값들도 겹칠 확률이 높다. 그래서 전체를 비교하지 않고 이 짧은 서명만 비교해 유사한 후보를 찾는다.

이 발상은 M10에서 다시 만난다. 전체를 비교하는 대신 값싼 서명으로 후보를 좁히고 정밀 검증한다는 2단계 패턴이 근사 최근접 검색과 정확히 같다.

백엔드의 언어로

중복 제거는 대규모 유사 항목 탐지이고, 정확 매칭은 해시 조인, 근사 매칭은 서명 기반 후보 생성 후 검증이다. 데이터 파이프라인에서 흔히 다루는 문제이고, 여기서 특별한 것은 규모뿐이다.

그리고 이 세션의 교훈은 데이터 엔지니어링의 일반 원칙과 같다. 더 많은 데이터가 항상 더 좋은 것은 아니다. 품질과 다양성이 양보다 중요할 때가 많고, 특히 중복은 양을 부풀리면서 가치를 더하지 않는다.

확인 질문

  1. 중복을 제거해 데이터를 줄였는데 성능이 오르는 이유를 두 가지 말해 보라.
  2. 정확히 같지 않고 거의 같은 문서는 어떻게 찾는가? 기본 발상을 설명해 보라.
  3. 중복이 평가를 오염시키는 경로를 설명해 보라.
확인

1. 첫째, 반복 노출로 인한 암기가 줄어 일반화가 개선된다. 둘째, 같은 내용에 낭비되던 계산 예산이 새로운 내용으로 돌아간다.

2. 문서를 겹치는 조각으로 나눠 해시하고 그중 최솟값 몇 개만 서명으로 남긴다. 많이 겹치는 문서는 이 서명도 겹칠 확률이 높으므로, 짧은 서명 비교로 후보를 좁힌 뒤 정밀 검증한다.

3. 중복이 많으면 학습셋과 검증셋에 같은 내용이 들어갈 확률이 커진다. 그러면 검증 손실이 실제보다 낙관적으로 나와 과적합을 탐지하지 못한다.


Session 66합성 데이터

모델이 만든 데이터로 모델을 학습시키는 것이 이제 표준 도구가 되었다. 검증할 수 있는 영역에서는 강력하고, 검증할 수 없는 영역에서는 위험하다는 것이 지금까지의 교훈이다.

왜 필요해졌는가

고품질 텍스트가 유한하다는 인식이 배경이다. 웹에서 걸러 낼 수 있는 양에는 한계가 있고, 책과 논문은 저작권 문제가 있으며, 특정 분야는 애초에 텍스트가 적다. 그런데 scaling law는 더 많은 데이터를 원한다.

동시에 모델이 충분히 좋아졌다. 강한 모델이 생성한 텍스트가 평균적인 웹 문서보다 품질이 높은 경우가 생겼고, 그렇다면 그것을 학습에 쓰지 못할 이유가 없다는 논리가 성립한다.

어디에 쓰이는가

위험

판단 기준

정리하면 이렇다. 생성된 데이터를 자동으로 검증할 수 있는가가 갈림길이다.

검증 가능하면 강력한 도구다. 수학, 코드, 형식 준수, 구조화된 추출 같은 영역이 여기 속한다.

검증 불가능하면 신중해야 한다. 사실 서술, 의견, 창작 영역에서는 사람 검수가 필수이고, 그렇다면 비용 이점이 크게 줄어든다.

이 기준은 M6과 M11에서 계속 다시 나온다.

확인 질문

  1. 합성 데이터가 특히 잘 작동하는 영역의 공통점은 무엇인가?
  2. 모델 붕괴가 무엇이며 왜 일어나는가?
  3. 합성 데이터를 쓸지 판단하는 핵심 기준 하나를 말해 보라.
확인

1. 정답을 프로그램으로 검증할 수 있다는 점이다. 수학, 코드, 형식 준수 같은 영역에서는 생성한 것 중 맞는 것만 걸러 낼 수 있어 오류가 섞이지 않는다.

2. 합성 데이터로 반복 학습할 때 분포의 꼬리가 사라지고 다양성이 줄어드는 현상이다. 모델이 자기 분포의 중심 근처를 주로 생성하기 때문에, 그것을 다시 학습하면 중심이 더 강화된다.

3. 생성된 데이터를 자동으로 검증할 수 있는가다. 검증 가능하면 강력하고, 불가능하면 사람 검수가 필요해 비용 이점이 사라진다.


Session 67scaling law

모델 크기, 데이터 양, 계산량을 늘리면 손실이 예측 가능한 멱법칙을 따라 줄어든다. 이것이 발견이자 도구다. 작은 실험으로 큰 실행의 결과를 예측해 예산을 배분할 수 있게 되었다.

관찰

여러 크기의 모델을 여러 양의 데이터로 학습시켜 손실을 재면, 로그-로그 축에서 거의 직선이 나온다. 즉 손실이 멱법칙을 따른다.

L(N)L+ANα

N이 파라미터 수, L_∞가 아무리 키워도 도달할 수 없는 하한, α가 감소 속도를 정하는 지수다. 데이터 양과 계산량에 대해서도 같은 형태의 관계가 성립한다.

이 관계가 놀라운 이유는 범위가 넓다는 점이다. 파라미터가 몇 자릿수에 걸쳐 변해도 같은 직선 위에 놓인다. 그래서 작은 규모에서 계수를 추정하면 훨씬 큰 규모의 결과를 예측할 수 있다.

도구로서의 의미

이 예측 가능성이 대규모 학습을 공학으로 만들었다.

수백만 달러를 들여 큰 모델을 학습시키기 전에, 작은 모델 몇 개로 곡선을 그려 본다. 그 곡선이 목표 손실에 도달할지, 도달한다면 얼마의 예산이 필요한지를 계산한다. 예측이 나쁘게 나오면 학습을 시작하지 않는다.

M2 세션 33에서 본 실험 관리가 여기서 큰 규모로 확장된다. 여러 크기의 실험을 같은 조건에서 돌리고, 그 결과를 곡선으로 맞추고, 외삽한다.

한계

이 도구에는 알려진 한계가 있다.

확인 질문

  1. scaling law가 예측 도구로 쓰인다는 것은 구체적으로 무슨 뜻인가?
  2. 손실이 예측대로 줄어도 원하는 능력이 좋아진다는 보장이 없는 이유는?
  3. 데이터 품질을 개선하는 것은 scaling law에서 어떤 위치를 차지하는가?
확인

1. 작은 규모의 실험 몇 개로 멱법칙의 계수를 추정한 뒤, 훨씬 큰 규모에서의 손실을 외삽해 예측한다. 그 예측으로 예산을 배분하고 학습 여부를 결정한다.

2. scaling law가 예측하는 것은 다음 토큰 예측 손실이고, 우리가 원하는 것은 특정 과제의 성능이다. 두 지표의 관계가 매끄럽지 않을 수 있다.

3. 곡선의 계수 안에 들어 있다. 같은 규모라도 데이터가 좋으면 곡선 전체가 아래로 내려간다. 즉 데이터 개선은 곡선 위를 이동하는 것이 아니라 곡선 자체를 옮기는 별개의 축이다.

앉아서 읽을 것

Chinchilla 논문의 1절과 3절이 다음 세션의 원문이다. 그래프 두어 개만 봐도 이 세션과 다음 세션의 요지가 한눈에 들어온다.


Session 68Chinchilla — 최적 배분

계산 예산이 정해져 있을 때 모델을 키울 것인가 데이터를 늘릴 것인가. Chinchilla의 답은 둘을 비슷한 비율로 함께 늘리라는 것이었고, 그 전까지 모델들이 데이터를 심각하게 적게 먹고 있었다는 사실이 드러났다.

문제 설정

계산량은 대략 C6ND다. N은 파라미터 수, D는 학습 토큰 수다. M1 세션 9에서 본 "파라미터당 2 FLOPs"에 역전파 몫까지 합친 값이다.

예산 C가 고정되어 있으면 ND는 반비례한다. 모델을 두 배로 키우면 데이터를 절반으로 줄여야 한다. 그러면 어느 조합이 손실을 가장 낮게 만드는가.

Chinchilla의 실험은 여러 조합을 실제로 학습시켜 손실을 비교한 것이다. 결론은 ND를 거의 같은 비율로 늘려야 한다는 것이었다. 구체적으로는 파라미터 하나당 토큰 20개 정도가 최적에 가깝다.

이 결과가 충격적이었던 이유는 당시 모델들이 이 비율에서 크게 벗어나 있었기 때문이다. 175B 모델을 300B 토큰으로 학습시킨 경우, 비율이 파라미터당 1.7 토큰이다. 최적의 10분의 1 수준이다. 같은 계산 예산으로 더 작은 모델을 훨씬 많은 데이터로 학습시켰다면 더 좋은 모델이 나왔을 것이라는 뜻이다.

실제로 Chinchilla는 70B 모델을 1.4T 토큰으로 학습시켜, 훨씬 큰 모델들보다 좋은 성능을 냈다.

같은 예산, 다른 배분

20
계산 예산을 고정하고 배분만 바꾼다. 왼쪽 끝은 큰 모델을 적은 데이터로, 오른쪽 끝은 작은 모델을 많은 데이터로 학습시키는 쪽이다. 골짜기 바닥이 최적 배분이고, 그 지점이 대략 파라미터당 20토큰이다.

추론 비용이라는 반전

여기에 실무적으로 중요한 반전이 있다. Chinchilla의 최적은 학습 비용만 고려한 것이다.

그런데 모델은 한 번 학습하고 수억 번 추론한다. 추론 비용은 모델 크기에 비례하므로, 총 소유 비용으로 보면 더 작은 모델을 최적보다 훨씬 많은 데이터로 학습시키는 것이 유리하다. 학습에서 손해를 보더라도 추론에서 계속 이득을 보기 때문이다.

이것이 최근 모델들이 파라미터당 수백에서 수천 토큰을 먹이는 이유다. Chinchilla 최적을 한참 넘어선 지점이지만, 총 비용 관점에서는 합리적이다. 이 지점을 지나면 성능 향상이 아주 완만해지지만 0은 아니고, 추론 비용 절감이 그것을 상쇄한다.

실무 판단으로

이 세션에서 가져갈 감각은 이것이다.

모델을 고를 때 파라미터 수만 보지 말고 학습 토큰 수를 함께 봐야 하는 이유다. 그리고 작은 모델이 큰 모델과 비슷한 성능을 내는 경우, 대개 데이터를 훨씬 많이 먹었기 때문이다.

확인 질문

  1. 계산량, 파라미터 수, 토큰 수의 관계식을 써 보라.
  2. Chinchilla의 결론을 한 문장으로 말해 보라.
  3. 최근 모델들이 Chinchilla 최적보다 훨씬 많은 데이터를 먹이는 이유는?
확인

1. C6ND. 파라미터 하나당 순전파 2 FLOPs에 역전파 몫을 더해 대략 6이 된다.

2. 계산 예산이 정해져 있으면 모델 크기와 학습 토큰 수를 비슷한 비율로 함께 늘려야 하고, 대략 파라미터당 20토큰이 최적에 가깝다.

3. Chinchilla 최적은 학습 비용만 고려한 것인데, 실제로는 추론이 훨씬 많이 일어난다. 추론 비용은 모델 크기에 비례하므로, 더 작은 모델을 더 많은 데이터로 학습시키는 편이 총 비용에서 유리하다.


Session 69사전학습의 비용 구조

계산 예산을 실제 돈과 시간으로 환산해 보면 왜 대부분의 조직이 사전학습을 하지 않는지가 분명해진다. 그리고 그 대안들이 각각 얼마나 싼지도 함께 보인다.

계산해 보기

앞 세션의 C6ND를 써서 8B 모델을 15T 토큰으로 학습시키는 비용을 추정해 보자.

C=6×8×109×15×1012=7.2×1023;FLOPs

H100 한 장의 bf16 실효 처리량을 400 TFLOPs 정도로 잡자. 이론 최대치가 아니라 실제로 달성되는 값이다.

7.2×10234×1014=1.8×109;57;GPU-년

GPU 1024장이면 약 20일이다. 시간당 임대료를 장당 2달러로 잡으면 1024×480시간×2달러100달러다. 여기에 실패한 시도, 데이터 준비, 인건비가 더해진다.

이 계산은 대략적이지만 자릿수 감각을 준다. 8B급 모델 하나에 백만 달러 단위이고, 70B급이면 자릿수가 하나 올라간다.

대안들의 비용

같은 척도로 대안들을 놓아 보면 판단이 쉬워진다.

방법대략적인 비용무엇을 바꾸는가
사전학습백만 달러 이상모델의 능력 전반
계속학습수천~수만 달러도메인·언어 적응
전체 파인튜닝수백~수천 달러행동과 형식
LoRA 파인튜닝수십~수백 달러행동과 형식
RAG 구축개발 비용 위주참조하는 지식
프롬프트 개선거의 무료즉각적인 행동

M0 세션 4에서 "가능한 한 위층에서 풀라"고 한 원칙이 여기서 숫자를 갖는다. 아래로 내려갈수록 비용이 자릿수 단위로 뛴다.

사전학습을 고려할 만한 경우

그럼에도 사전학습이 답인 경우가 있다.

확인 질문

  1. 8B 모델을 15T 토큰으로 학습시키는 데 필요한 계산량을 계산해 보라.
  2. 사전학습과 LoRA 파인튜닝의 비용 차이는 대략 몇 자릿수인가?
  3. 파인튜닝으로 해결되지 않아 사전학습이 필요한 경우를 하나 들어 보라.
확인

1. 6×8e9×15e12=7.2e23 FLOPs다.

2. 대략 네 자릿수 이상이다. 사전학습이 백만 달러 단위, LoRA 파인튜닝이 수십에서 수백 달러 단위다.

3. 토크나이저가 해당 언어를 심각하게 비효율적으로 처리하는 경우다. 토크나이저를 바꾸면 임베딩이 무의미해지므로 사전학습을 다시 해야 한다.


Session 70창발 능력 논쟁

모델을 키우다 보면 어떤 능력이 갑자기 나타나는 것처럼 보인다. 이것이 실제 상전이인지, 아니면 우리가 쓰는 지표가 만든 착시인지에 대한 논쟁이 있고, 그 결론이 실무의 평가 설계에 직접 영향을 준다.

관측

여러 과제에서 비슷한 패턴이 보고되었다. 모델 크기를 키워도 성능이 무작위 수준에 머물다가, 어느 규모를 넘어서면 갑자기 크게 오른다. 여러 자릿수 산술, 복합 추론, 지시 이해 같은 과제들이다.

이 관측이 널리 인용되면서 "규모를 키우면 예측할 수 없는 능력이 나타난다"는 서사가 만들어졌다. 정책적 함의도 컸다. 다음 세대 모델이 무엇을 할 수 있을지 예측할 수 없다는 주장의 근거가 되었기 때문이다.

반론

이후 이 관측이 지표의 성질 때문일 수 있다는 반론이 제기되었다.

핵심 논거는 이렇다. 세 자릿수 덧셈의 정확도를 잰다고 하자. 이것은 모든 자리가 맞아야 1점, 하나라도 틀리면 0점인 이진 지표다. 모델이 조금씩 나아지고 있어도, 완전히 맞을 확률은 어느 지점까지 거의 0으로 유지되다가 급격히 오른다. 각 자리를 맞힐 확률이 조금씩 올라가는데 그것들을 곱하면 급격한 곡선이 나오기 때문이다.

같은 과제를 부분 점수를 주는 지표로 재면 곡선이 부드러워진다. 즉 모델의 능력은 매끄럽게 향상되고 있었는데, 우리가 쓴 지표가 그것을 계단으로 보이게 만들었다는 것이다.

어느 쪽인가

두 입장이 완전히 배타적이지는 않다. 상당수의 창발 사례가 지표 효과로 설명되는 것은 사실이고, 동시에 모든 사례가 그렇게 설명되지는 않는다는 반론도 있다.

실무자에게 중요한 것은 결론이 아니라 여기서 나오는 방법론적 교훈이다.

실무로 가져올 것

이 교훈들은 M12의 평가 설계로 그대로 이어진다.

확인 질문

  1. 창발 능력이 지표의 착시일 수 있다는 주장의 논거를 설명해 보라.
  2. 이진 지표가 개선을 숨기는 이유를 확률의 곱으로 설명해 보라.
  3. 이 논쟁에서 실무로 가져올 교훈 하나를 말해 보라.
확인

1. 완전 정답만 인정하는 이진 지표를 쓰면, 부분적인 능력이 매끄럽게 향상되고 있어도 완전 정답 확률은 어느 지점까지 거의 0에 머물다 급격히 오른다. 지표를 부분 점수로 바꾸면 곡선이 부드러워진다.

2. 여러 부분이 모두 맞아야 성공인 과제에서 성공 확률은 각 부분의 확률을 곱한 값이다. 각 확률이 0.5에서 0.9로 선형으로 올라도 여러 개를 곱하면 결과는 급격한 곡선이 된다.

3. 이진 지표만 보지 말고 부분 점수나 중간 단계 지표를 함께 봐야 개선의 방향을 일찍 판단할 수 있다.


Session 71계속학습

사전학습된 모델에서 출발해 도메인 데이터로 사전학습을 더 하는 것이다. SFT와 목적이 다르고 RAG와도 다르며, 셋 중 무엇을 골라야 하는지에는 분명한 기준이 있다.

SFT와 무엇이 다른가

계속학습은 형식이 사전학습과 같다. 그냥 텍스트를 넣고 다음 토큰을 예측하게 한다. 지시-응답 쌍이 아니라 도메인 문서 자체를 먹인다.

M6에서 볼 SFT는 다르다. 지시와 응답의 쌍을 주고 응답 부분에서만 손실을 계산한다. 목적이 행동을 가르치는 것이다.

계속학습은 지식과 언어 감각을 넣고, SFT는 행동을 가르친다. 순서도 이렇게 간다. 계속학습을 한 뒤 SFT를 한다. 반대로 하면 계속학습이 SFT로 얻은 대화 능력을 지워 버린다.

언제 쓰는가

계속학습이 답인 경우는 대체로 이렇다.

위험

셋 중 고르기

상황
자주 바뀌는 사실, 출처 표시 필요RAG
응답 형식·말투·절차를 일정하게SFT
도메인의 어휘와 문체 자체가 낯섦계속학습 후 SFT
위 여러 개가 동시에계속학습 → SFT → RAG를 함께

마지막 줄이 중요하다. 이들은 배타적이지 않다. 실제 시스템은 도메인 적응된 모델 위에 RAG를 얹는 식으로 조합한다.

확인 질문

  1. 계속학습과 SFT의 목적 차이를 한 문장씩으로 말해 보라.
  2. 계속학습에서 일반 데이터를 섞는 이유는?
  3. 자주 바뀌는 사내 규정을 다루는 데 계속학습이 적절하지 않은 이유는?
확인

1. 계속학습은 도메인의 지식과 언어 감각을 넣는 것이고, SFT는 지시를 따르는 행동과 응답 형식을 가르치는 것이다.

2. 새 도메인 데이터만 넣으면 원래의 일반 능력이 퇴화하는 파국적 망각이 일어나기 때문이다. 원래 분포의 데이터를 일정 비율 섞어 이를 완화한다.

3. 파라미터에 들어간 지식은 바뀔 때마다 재학습해야 하고 출처를 밝힐 수도 없다. 자주 바뀌고 근거 제시가 필요한 정보는 RAG로 다루는 것이 맞다.


Session 72복습 — 예산과 데이터의 판단

이 모듈의 목표는 사전학습을 할 줄 아는 것이 아니라, "이 문제를 어느 층에서 풀 것인가"에 숫자를 붙여 판단하는 것이다.

이어 보기

사전학습은 M4에서 본 구조에 인터넷 규모의 텍스트를 흘려보내며 다음 토큰 예측 하나만 최적화하는 일이다. 알고리즘은 M2에서 배운 그대로이고, 다른 것은 규모다. 규모가 데이터 파이프라인, 장애 대응, 체크포인트 관리를 공학 문제로 만든다.

데이터는 웹 크롤에서 시작해 언어 판별, 품질 필터, 중복 제거, 오염 제거를 거쳐 도메인 비율에 맞춰 섞인다. 이 중 중복 제거는 데이터를 줄이는데도 성능을 올리는 드문 조치다. 암기를 막고 계산 예산을 아끼기 때문이다. 부족한 데이터는 합성으로 채우는데, 자동 검증이 가능한 영역에서만 안전하다.

규모와 성능의 관계는 멱법칙을 따르고, 이 예측 가능성이 대규모 학습을 공학으로 만들었다. 계산 예산이 C6ND로 정해지면 ND를 어떻게 나눌지가 문제인데, Chinchilla의 답은 비슷한 비율로 함께 늘리라는 것이었다. 다만 추론 비용까지 포함하면 더 작은 모델을 더 많은 데이터로 학습시키는 편이 유리해서, 최근 모델들은 그 방향으로 갔다.

비용을 실제로 계산해 보면 8B 모델 하나에 백만 달러 단위다. 그래서 대부분의 조직에게 선택지는 계속학습, 파인튜닝, RAG이고, 아래로 갈수록 비용이 자릿수 단위로 뛴다.

자가 점검

  1. 중복 제거가 데이터를 줄이는데도 성능을 올리는 이유 두 가지는? (S65)
  2. 합성 데이터가 안전한 영역과 위험한 영역을 가르는 기준은? (S66)
  3. C6ND에서 6은 어디서 나온 숫자인가? (S68)
  4. Chinchilla 최적을 넘어 데이터를 더 먹이는 것이 합리적인 이유는? (S68)
  5. 같은 8B 모델이라도 성능이 다를 수 있는 이유를 학습 토큰 수로 설명해 보라. (S68)
  6. 이진 지표가 개선을 숨기는 메커니즘과, 그것이 평가 설계에 주는 교훈은? (S70)
  7. 계속학습과 SFT의 순서가 정해져 있는 이유는? (S71)
  8. "사내 규정을 모른다"는 문제에 RAG가 계속학습보다 나은 이유 두 가지는? (S71)
확인

1. 반복 노출로 인한 암기가 줄어 일반화가 개선되고, 같은 내용에 낭비되던 계산 예산이 새 내용으로 돌아간다.

2. 생성된 데이터를 자동으로 검증할 수 있는가다. 수학·코드·형식처럼 검증 가능한 영역은 안전하고, 사실 서술이나 의견처럼 검증 불가능한 영역은 오류와 편향이 증폭될 위험이 있다.

3. 순전파에서 파라미터당 약 2 FLOPs가 들고, 역전파가 그 두 배 정도이므로 합쳐서 약 6이다.

4. Chinchilla 최적은 학습 비용만 고려한 것인데, 모델은 한 번 학습하고 수억 번 추론한다. 추론 비용이 모델 크기에 비례하므로 더 작은 모델을 더 많이 학습시키는 편이 총비용에서 유리하다.

5. 모델 크기는 능력의 상한을 정하고 학습 토큰 수는 그 상한에 얼마나 도달했는지를 정한다. 2T 토큰을 본 8B와 15T 토큰을 본 8B는 다른 물건이다.

6. 여러 부분이 모두 맞아야 성공인 지표는 부분 확률의 곱이라, 능력이 매끄럽게 향상되어도 지표는 계단처럼 움직인다. 교훈은 최종 성공률과 함께 중간 단계 지표와 부분 점수를 봐야 한다는 것이다.

7. 계속학습은 일반 텍스트로 하는 사전학습이라, SFT 이후에 하면 SFT로 얻은 대화 능력을 지운다. 그래서 계속학습을 먼저 하고 SFT를 나중에 한다.

8. 첫째, 규정이 바뀔 때마다 재학습하지 않고 문서만 갱신하면 된다. 둘째, 어느 문서에서 나온 답인지 근거를 함께 제시할 수 있다.

모듈을 마치며

지금까지는 모델이 만들어지는 이야기였다. 그런데 사전학습만 마친 모델은 아직 쓸 수 없다. 질문을 하면 답 대신 비슷한 질문을 나열하고, 언제 멈춰야 하는지도 모른다. M0 세션 2에서 말한 그 간극이다.

다음 모듈이 그 간극을 메운다. 베이스 모델을 대화할 수 있는 물건으로 바꾸는 과정, 그리고 그 과정에서 무엇을 할 수 있고 무엇을 할 수 없는지를 열여섯 번의 이동에 걸쳐 본다.

M6세션 73–8816개

사후학습

베이스 모델을 쓸 수 있는 물건으로 바꾸는 과정이다. 여기서 무엇을 할 수 있고 무엇을 할 수 없는지의 경계를 아는 것이 이 모듈의 목표다.

Session 73베이스 모델과 채팅 모델의 차이

목적함수는 같고 데이터가 다르다. 그런데 그 차이가 완전히 다른 물건을 만든다. 베이스 모델은 텍스트를 이어 쓰고, 채팅 모델은 질문에 답한다.

베이스 모델의 행동

M5를 마친 모델에게 "대한민국의 수도는 어디인가요?"라고 물으면 무슨 일이 벌어질까.

운이 좋으면 "서울입니다"라고 답한다. 그런데 이런 답도 흔하다.

`` 대한민국의 수도는 어디인가요? 일본의 수도는 어디인가요? 중국의 수도는 어디인가요? ``

버그가 아니다. 인터넷에는 질문 목록이 흔하고, 질문 뒤에 또 다른 질문이 오는 것은 통계적으로 매우 자연스러운 이어쓰기다. M0 세션 2에서 말한 대로 사전학습의 목표는 유용한 답이 아니라 그럴듯한 이어쓰기다.

다른 증상도 있다. 답을 하더라도 멈추지 않고 계속 쓴다. 언제 끝내야 하는지 배운 적이 없기 때문이다. 대화 상대의 발화를 자기가 지어내기도 한다. 대화 기록 형태의 텍스트를 학습했으니 그것도 자연스러운 이어쓰기다.

무엇을 가르쳐야 하는가

정리하면 베이스 모델에는 세 가지가 없다.

이것들은 지식이 아니라 행동이다. 그래서 사후학습이 필요하다.

능력은 이미 있다

중요한 것은 사후학습이 새 능력을 만들지 않는다는 점이다. 베이스 모델도 번역, 요약, 코드 작성을 할 수 있다. 적절한 형식으로 프롬프트를 주면 나온다. 다만 그 형식을 사용자가 알아내야 하고, 결과를 잘라 내야 하고, 매번 다르게 나온다.

사후학습은 그 능력을 꺼내 쓰기 쉬운 인터페이스로 포장하는 일이다. 그래서 사후학습으로 성능이 크게 오르는 것처럼 보이지만, 실제로는 이미 있던 것에 접근하는 방법이 쉬워진 것에 가깝다.

이 사실이 실무 판단에 주는 함의가 크다. 베이스 모델에 없는 능력은 사후학습으로 만들 수 없다. 사전학습 데이터에 한국어가 거의 없었다면 SFT로 한국어를 잘하게 만들 수 없다. 그때는 M5 세션 71의 계속학습이 필요하다.

확인 질문

  1. 베이스 모델에게 질문했을 때 또 다른 질문이 나오는 것이 왜 자연스러운 행동인가?
  2. 사후학습이 가르치는 세 가지 행동은?
  3. "사후학습이 새 능력을 만들지 않는다"는 말이 실무 판단에 주는 함의는?
확인

1. 사전학습의 목표는 그럴듯한 이어쓰기이고, 인터넷에는 질문 뒤에 질문이 이어지는 문서가 흔하기 때문이다. 통계적으로 자연스러운 선택이다.

2. 질문에 응답한다는 것, 답이 끝나면 멈춘다는 것, 그리고 자기 발화와 상대 발화의 역할을 구분한다는 것이다.

3. 베이스 모델에 없는 능력은 사후학습으로 만들 수 없다는 뜻이다. 예컨대 한국어 데이터가 거의 없이 학습된 모델은 SFT로 한국어를 잘하게 만들 수 없고, 계속학습이 필요하다.


Session 74SFT

지시와 응답의 쌍을 모아 응답 부분에서만 다음 토큰 예측을 학습시킨다. 목적함수는 사전학습과 완전히 같고, 데이터와 마스킹만 다르다.

절차

데이터는 이런 형태다.

`` 지시: 다음 문장을 영어로 번역해 주세요. "오늘 날씨가 좋다" 응답: The weather is nice today. ``

이것을 M3 세션 41에서 본 chat template으로 펼쳐 하나의 토큰 배열로 만든다. 그다음 사전학습과 똑같이 다음 토큰 예측 손실을 계산한다. 다른 점은 손실을 응답 구간에서만 계산한다는 것뿐이다. 이유는 다음 세션에서 따로 다룬다.

이렇게 수천에서 수십만 건을 학습시키면 모델의 행동이 바뀐다. 질문을 받으면 답을 생성하고, 답이 끝나면 종료 토큰을 낸다.

데이터가 전부다

SFT에서 결과를 좌우하는 것은 알고리즘이 아니라 데이터다. 몇 가지 원칙이 반복적으로 확인되었다.

얼마나 학습시키는가

M2 세션 32에서 본 대로 파인튜닝은 과적합이 실제 문제가 되는 영역이다. 데이터가 수천 건인데 모델이 거대하므로 몇 번만 반복해도 외운다.

실무 설정은 대체로 이렇다. 에폭은 1에서 3, 학습률은 사전학습보다 한두 자릿수 작게, 검증 손실을 매 에폭 확인하고 올라가기 시작하면 멈춘다.

확인 질문

  1. SFT의 목적함수는 사전학습과 무엇이 같고 무엇이 다른가?
  2. "모델이 모르는 사실에 대한 정확한 답"을 SFT 데이터에 넣으면 왜 환각이 늘어나는가?
  3. SFT에서 에폭을 1~3으로 적게 잡는 이유는?
확인

1. 목적함수 자체는 다음 토큰 예측의 cross-entropy로 완전히 같다. 다른 것은 데이터가 지시-응답 쌍이라는 점과, 손실을 응답 구간에서만 계산한다는 점이다.

2. 모델은 그 사실을 배우는 것이 아니라 "이런 질문에는 자신 있게 구체적으로 답한다"는 태도를 배우기 때문이다. 모르는 영역에서도 같은 태도로 답하게 되어 환각이 늘어난다.

3. 데이터가 수천에서 수만 건 규모인데 모델은 거대해서 몇 번만 반복하면 데이터를 외운다. 외우기 시작하면 학습한 예제와 조금만 달라도 성능이 떨어진다.


Session 75loss masking

프롬프트 구간에 손실을 걸면 모델이 질문을 생성하는 법을 배운다. 우리가 원하는 것은 답을 생성하는 능력이므로 응답 구간의 손실만 쓴다. 한 줄짜리 설정이지만 빼먹으면 결과가 눈에 띄게 나빠진다.

왜 그런가

토큰 배열은 이렇게 생겼다.

`` [시스템] 너는 도움이 되는 비서다 [사용자] 오늘 날씨는? [어시스턴트] 오늘은 맑습니다 [종료] ``

여기에 모든 위치의 손실을 걸면 어떻게 될까. 모델은 "[사용자] 다음에는 '오늘'이 온다", "'오늘' 다음에는 '날씨는'이 온다" 같은 것도 학습한다. 즉 사용자가 어떤 질문을 하는지를 예측하는 능력이 학습된다.

이것이 해로운 이유는 두 가지다. 학습 용량이 우리가 원하지 않는 것에 쓰인다. 그리고 데이터셋에 특정 유형의 질문이 많으면 모델이 그 질문을 스스로 생성하려는 경향이 생긴다. 실제로 마스킹을 빼먹으면 모델이 답을 하고 나서 다음 질문을 지어내는 증상이 나타난다.

어떻게 하는가

응답 구간 밖의 위치에 대해 손실을 무시하도록 라벨을 표시한다. 대개 특별한 값으로 라벨을 채우고 손실 함수가 그 값을 건너뛰게 한다.

주의할 점은 경계 처리다. 어시스턴트 발화의 시작을 알리는 토큰까지는 마스킹하고, 실제 응답 내용부터 종료 토큰까지를 학습 대상으로 삼는다. 종료 토큰을 학습 대상에서 빼면 모델이 멈추는 법을 배우지 못한다. 이것도 흔한 실수다.

멀티턴 대화라면 어시스턴트 발화가 여러 번 나온다. 각각을 모두 학습 대상으로 삼는 것이 일반적이고, 그러면 한 샘플에서 여러 개의 학습 신호를 얻는다.

검증하는 법

이 설정이 맞는지 확인하는 방법은 단순하다. 마스킹된 라벨을 실제로 디코딩해 보는 것이다. 손실을 계산하는 위치의 토큰만 뽑아 텍스트로 되돌리면, 어시스턴트의 응답만 나와야 한다. 시스템 프롬프트나 사용자 발화가 섞여 있으면 마스킹이 잘못된 것이다.

이 확인은 5분이면 되고, 하지 않으면 학습을 다 돌린 뒤에야 이상하다는 것을 알게 된다. M2 세션 33의 "학습을 시작하자마자 확인할 것" 목록에 이 항목을 추가해 두면 좋다.

예외

프롬프트에도 손실을 거는 것이 나은 경우가 있다. 학습 데이터가 아주 적고 모델을 특정 도메인의 언어에 익숙하게 만들고 싶다면, 프롬프트의 언어 자체가 학습할 가치가 있는 신호일 수 있다. 다만 이것은 예외이고 기본값은 마스킹이다.

확인 질문

  1. 프롬프트 구간에 손실을 걸면 모델이 무엇을 배우게 되는가?
  2. 종료 토큰을 학습 대상에서 빼면 무슨 일이 생기는가?
  3. 마스킹이 제대로 됐는지 확인하는 가장 확실한 방법은?
확인

1. 사용자가 어떤 질문을 하는지를 예측하는 능력, 즉 질문을 생성하는 법을 배운다. 학습 용량이 낭비되고 답변 후에 질문을 지어내는 증상이 나타난다.

2. 모델이 언제 멈춰야 하는지 배우지 못해 응답이 끝나지 않고 계속 이어진다.

3. 손실을 계산하는 위치의 토큰만 뽑아 텍스트로 디코딩해 보는 것이다. 어시스턴트 응답만 나와야 하고, 시스템이나 사용자 발화가 섞이면 잘못된 것이다.


Session 76왜 SFT만으로는 부족한가

SFT는 "이렇게 답하라"는 본보기를 흉내 내게 한다. 그런데 여러 답이 모두 가능할 때 무엇이 더 나은지는 본보기 하나로 가르칠 수 없다. 그 간극을 메우는 것이 선호 최적화다.

본보기의 한계

SFT 데이터에는 각 지시에 하나의 응답이 붙어 있다. 모델은 그 응답의 확률을 높이도록 학습된다.

문제는 좋은 답이 하나가 아니라는 점이다. "이 코드를 설명해 주세요"에 대한 좋은 답은 수없이 많고, 나쁜 답도 수없이 많다. 본보기 하나를 흉내 내게 하는 것으로는 좋은 답의 공간 전체를 가르칠 수 없다.

더 근본적인 문제는 나쁜 답을 명시적으로 억제할 수단이 없다는 것이다. cross-entropy는 정답의 확률을 높일 뿐이고, 특정 오답을 낮추라고 말하지 않는다. 정답 확률이 올라가면 다른 것들이 자동으로 내려가긴 하지만, 어떤 것을 특히 더 내려야 하는지는 지정할 수 없다.

비교는 쉽다

여기서 실용적인 관찰이 나온다. 좋은 답을 쓰는 것보다 두 답 중 어느 쪽이 나은지 고르는 것이 훨씬 쉽다.

사람에게 "이 질문에 이상적인 답을 작성하세요"라고 하면 시간이 많이 들고 품질도 들쭉날쭉하다. 그런데 "이 두 답 중 어느 쪽이 낫습니까"라고 물으면 빠르고 일관성도 높다.

그리고 비교 데이터에는 SFT 데이터에 없는 정보가 들어 있다. 더 나쁜 쪽이 무엇인지가 명시되어 있다. 이 정보를 학습에 쓸 수 있다면 나쁜 방향을 직접 억제할 수 있다.

세 갈래

선호 데이터를 쓰는 방법에는 크게 세 갈래가 있고, 이후 세션들에서 차례로 다룬다.

확인 질문

  1. SFT가 나쁜 답을 억제하지 못하는 이유를 손실 함수로 설명해 보라.
  2. 사람에게 답을 쓰게 하는 것보다 비교하게 하는 것이 나은 이유 두 가지는?
  3. 선호 데이터에 있고 SFT 데이터에 없는 정보는 무엇인가?
확인

1. cross-entropy는 정답 토큰의 확률을 높이라는 지시만 담고 있다. 어떤 특정 응답을 특히 억제하라는 신호가 목적함수에 들어 있지 않다.

2. 첫째, 비교가 훨씬 빠르고 싸다. 둘째, 작성보다 판단의 일관성이 높아 라벨 품질이 좋다.

3. 더 나쁜 응답이 무엇인지에 대한 정보다. SFT 데이터에는 좋은 예만 있고 나쁜 예가 없다.


Session 77RLHF 전체 구조

선호 데이터 수집 → 보상 모델 학습 → 강화학습으로 정책 최적화의 3단계다. 각 단계에 서로 다른 모델이 등장해서 복잡하지만, 각각이 무슨 일을 하는지는 명확하다.

세 단계

왜 보상 모델이 필요한가

3단계에서 사람이 직접 채점하면 안 되는가. 원리적으로는 되지만 실행이 불가능하다. 강화학습은 수십만 번의 채점을 요구하는데 사람은 그 속도를 낼 수 없다.

보상 모델은 사람의 판단을 근사해서 무한히 빠르게 재현하는 장치다. 사람 라벨 수만 건으로 학습해 두면 이후 수백만 번 채점할 수 있다.

등장하는 모델들

여기가 RLHF를 복잡하게 만드는 지점이다. 3단계에서 동시에 네 개의 모델이 메모리에 있어야 한다.

즉 학습되는 모델이 둘, 고정된 모델이 둘이다. 모두 GPU 메모리를 차지하므로 같은 크기의 SFT보다 훨씬 무겁다. 이것이 이후 DPO와 GRPO가 각각 무엇을 제거했는지를 이해하는 배경이 된다.

확인 질문

  1. RLHF의 세 단계를 순서대로 말해 보라.
  2. 3단계에서 사람이 직접 채점하지 않고 보상 모델을 쓰는 이유는?
  3. PPO 기반 RLHF에서 메모리에 올라가는 모델 네 개는 각각 무엇인가?
확인

1. 선호 데이터 수집, 보상 모델 학습, 강화학습으로 정책 최적화다.

2. 강화학습이 수십만 번의 채점을 요구하는데 사람은 그 속도를 낼 수 없기 때문이다. 보상 모델은 사람의 판단을 근사해 무한히 빠르게 재현한다.

3. 학습되는 정책 모델과 가치 모델, 고정된 참조 모델과 보상 모델이다.

앉아서 읽을 것

InstructGPT 논문의 3절이 이 파이프라인의 원문이다. 그림 2 하나만 봐도 세 단계 구조가 정리된다.


Session 78보상 모델

보상 모델은 비교 데이터로 학습된 점수 함수다. 절대 점수 라벨 없이 상대 비교만으로 학습되고, 그래서 점수의 절대값에는 의미가 없다.

구조와 학습

보상 모델은 대개 언어 모델과 같은 구조를 쓰되 마지막 층만 바꾼다. 어휘 크기의 점수를 내는 대신 숫자 하나를 내도록 헤드를 교체한다.

학습은 이렇게 한다. 선택된 응답의 점수를 r_w, 거부된 응답의 점수를 r_l이라 하면, 손실은 이 형태다.

L=logσ(rwrl)

σ는 로지스틱 함수다. 이 손실은 r_w - r_l이 클수록 작아진다. 즉 선택된 쪽 점수를 거부된 쪽보다 높게 만들라는 지시다.

여기서 중요한 성질이 나온다. 손실이 두 점수의 차이에만 의존하므로, 모든 점수에 같은 상수를 더해도 손실이 변하지 않는다. M1 세션 14에서 본 softmax의 성질과 같은 구조다. 그래서 보상 점수의 절대값에는 의미가 없고 순서만 의미가 있다. 보상이 3.2라는 것 자체는 아무것도 말해 주지 않는다.

무엇이 어려운가

보상 모델이 RLHF의 병목이라는 것이 실무의 공통된 인식이다.

사람이 선호하는 것의 편향

보상 모델은 사람의 선호를 학습하므로 사람의 편향도 함께 학습한다. 가장 잘 알려진 것이 길이 편향이다. 사람은 긴 답을 더 좋게 평가하는 경향이 있고, 그래서 보상 모델도 긴 답에 높은 점수를 준다. 그 결과 RLHF를 거친 모델이 이유 없이 장황해지는 현상이 널리 관측되었다.

자신감 있는 어조, 목록 형식, 서론과 결론이 붙은 구조 같은 표면적 특징도 비슷한 편향의 대상이다. 내용이 아니라 형식이 점수를 올리는 경로가 열려 있는 것이다.

이 문제는 M12에서 LLM-as-a-Judge를 다룰 때 다시 나온다. 같은 종류의 편향이 그쪽에서도 작동한다.

확인 질문

  1. 보상 모델의 손실이 두 점수의 차이에만 의존한다는 것은 무엇을 함의하는가?
  2. 강화학습이 진행되면서 보상 모델의 신뢰도가 떨어지는 이유는?
  3. RLHF를 거친 모델이 장황해지는 현상의 원인은?
확인

1. 점수의 절대값에는 의미가 없고 순서만 의미가 있다는 뜻이다. 모든 점수에 상수를 더해도 학습 결과가 같다.

2. 보상 모델은 학습 시점 정책의 응답 분포로 학습되었는데, 강화학습이 진행되면 정책이 변해 보상 모델이 본 적 없는 응답이 나온다. 그 영역에서는 판단이 신뢰할 수 없다.

3. 사람이 긴 답을 더 좋게 평가하는 경향이 있고, 보상 모델이 그 편향을 학습해 길이에 점수를 주기 때문이다. 정책이 그 신호를 좇아 장황해진다.


Session 79PPO 개요

PPO는 정책을 한 번에 조금씩만 바꾸도록 제한하면서 보상을 높이는 강화학습 알고리즘이다. 오래 표준이었지만 복잡하고 무거워서, 지금은 더 단순한 대안들에 자리를 내주고 있다.

강화학습으로 보는 언어 생성

강화학습의 틀에 맞춰 보면 이렇게 대응된다. 모델이 정책이고, 지금까지 생성된 토큰들이 상태이고, 다음 토큰을 고르는 것이 행동이며, 응답이 끝났을 때 보상 모델이 주는 점수가 보상이다.

여기서 어려운 점이 드러난다. 보상은 응답이 다 끝난 뒤에 한 번 주어지는데, 그 응답은 수백 개의 토큰 선택으로 이루어져 있다. 어느 토큰 선택이 좋은 점수에 기여했는지 알 수 없다. 이것을 신용 할당 문제라고 부른다.

핵심 아이디어

PPO의 이름에 있는 "근접"이 핵심이다. 정책을 갱신할 때 이전 정책에서 너무 멀리 벗어나지 않도록 제한한다.

이유는 강화학습의 불안정성이다. 보상 신호는 노이즈가 크고, 한 번에 크게 움직이면 정책이 무너져 회복하지 못할 수 있다. 그래서 갱신 폭에 상한을 둔다.

여기에 가치 모델이 필요하다. 어떤 상태에서 앞으로 받을 보상의 기댓값을 추정해서, 실제 보상이 기대보다 나은지 나쁜지를 판단한다. 그 차이가 학습 신호가 된다. 절대 보상보다 이 상대적 차이를 쓰는 편이 학습이 안정적이다.

무엇이 문제였나

PPO 기반 RLHF는 실제로 잘 작동했지만 비용이 컸다.

이런 이유로 DPO 같은 단순한 대안이 등장했을 때 빠르게 채택되었다. 다만 PPO 계열이 완전히 대체된 것은 아니고, 세션 84의 RLVR처럼 보상이 명확한 영역에서는 여전히 강화학습 계열이 강하다.

확인 질문

  1. 언어 생성을 강화학습 틀에 대응시킬 때 상태, 행동, 보상은 각각 무엇인가?
  2. PPO의 "근접"이 뜻하는 것과 그것이 필요한 이유는?
  3. PPO 기반 RLHF의 실무적 부담을 두 가지 말해 보라.
확인

1. 상태는 지금까지 생성된 토큰들, 행동은 다음 토큰 선택, 보상은 응답이 끝났을 때 보상 모델이 주는 점수다.

2. 정책을 갱신할 때 이전 정책에서 너무 멀리 벗어나지 않게 제한한다는 뜻이다. 보상 신호의 노이즈가 커서 한 번에 크게 움직이면 정책이 무너질 수 있기 때문이다.

3. 모델 네 개가 동시에 메모리에 올라가야 하고, 하이퍼파라미터가 많고 서로 얽혀 조정이 어렵다. (생성이 학습 루프 안에 있어 느리다는 점, 재현이 어렵다는 점도 정답)


Session 80KL 제약의 역할

보상만 좇으면 모델이 보상 모델의 허점을 파고들어 언어 능력을 잃는다. 원래 모델에서 멀어지는 만큼 벌점을 물리는 것이 유일하고 효과적인 방어책이다.

reward hacking

앞 세션들에서 본 대로 보상 모델은 근사다. 어딘가에 반드시 실제 품질과 어긋나는 영역이 있다.

강화학습은 보상을 최대화하는 것이 목표이므로, 그런 영역을 적극적으로 찾아낸다. 그리고 찾으면 그쪽으로 몰려간다. 관측된 사례들은 이런 모습이다. 특정 문구를 반복해서 넣으면 점수가 오르는 것을 발견하고 모든 답에 그 문구를 붙인다. 극단적인 경우 의미 없는 문자열을 반복하는데 보상 모델이 높은 점수를 준다.

이것은 알고리즘의 버그가 아니라 목표를 정확히 달성한 결과다. 우리가 준 목표가 "보상 모델의 점수를 높여라"였고, 모델은 그것을 했다. M2 세션 25에서 본 "모델은 손실 함수에 적힌 것만 배운다"가 극단적인 형태로 나타난 것이다.

밧줄

해결책은 목적함수에 항을 하나 더하는 것이다.

목표=mathbbE[r(x,y)]β,KLbig(π,|,πrefbig)

앞의 항은 보상의 기댓값이다. 뒤의 항이 M1 세션 18에서 배운 KL divergence로, 현재 정책 π가 참조 모델 π_ref에서 얼마나 벗어났는지를 잰다. β가 그 벌점의 강도다.

의미는 명확하다. 보상을 높이되, 원래 모델에서 멀어진 만큼 대가를 치러라. 보상 모델의 허점은 대개 원래 모델의 분포에서 멀리 떨어진 이상한 영역에 있으므로, 그쪽으로 가는 것 자체에 비용을 물리면 접근이 억제된다.

참조 모델이 SFT를 마친 시점의 사본이라는 점도 중요하다. 그 모델은 언어 능력과 지시 이행 능력을 이미 갖추고 있으므로, 거기서 크게 벗어나지 않는다는 것은 그 능력을 유지한다는 뜻이 된다.

beta 조절

β는 두 목표 사이의 손잡이다.

실무에서는 이 값을 조절하며 보상 점수와 별도의 품질 지표를 함께 본다. 보상만 보면 hacking이 일어나는 순간을 놓친다. 예를 들어 응답 길이 분포, 반복률, 그리고 보상 모델과 무관한 별도의 평가셋 성능을 함께 추적한다.

이 대목은 M12의 평가와 직결된다. 최적화 대상 지표와 검증 지표를 분리하는 것이 원칙이다.

보상과 언어 능력의 줄다리기

0.10
β를 줄이면 보상은 계속 오르지만, 어느 지점부터 언어 능력이 급격히 무너진다. 보상 곡선만 보면 이 붕괴가 보이지 않는다는 것이 핵심이다. 최적화 대상과 검증 지표를 반드시 분리해야 하는 이유가 이 그림에 있다.

확인 질문

  1. reward hacking이 알고리즘의 버그가 아니라고 말하는 이유는?
  2. KL 항이 reward hacking을 막는 원리를 설명해 보라.
  3. 학습 중 보상 점수만 보면 안 되는 이유는?
확인

1. 우리가 준 목표가 "보상 모델의 점수를 높여라"였고 모델은 그것을 정확히 달성했기 때문이다. 문제는 그 목표가 진짜 목적의 불완전한 대리물이었다는 데 있다.

2. 보상 모델의 허점은 대개 원래 모델의 분포에서 멀리 떨어진 영역에 있다. 원래 모델에서 벗어나는 것 자체에 벌점을 물리면 그런 영역으로 가는 것이 억제된다.

3. 보상 점수는 hacking이 일어나도 계속 올라가기 때문이다. 보상 모델과 무관한 별도의 품질 지표를 함께 봐야 붕괴를 감지할 수 있다.


Session 81DPO

보상 모델과 강화학습을 한꺼번에 소거하고 선호 데이터로 언어 모델을 직접 학습시킨다. 수학적 유도가 핵심인데, 결과는 놀랄 만큼 단순한 손실 함수 하나다.

유도의 직관

RLHF의 목표는 세션 80의 식이었다. 보상을 최대화하되 KL 벌점을 문다.

여기에 알려진 사실이 하나 있다. 이 목표의 최적해가 닫힌 형태로 존재한다. 즉 최적 정책이 보상 함수로 명시적으로 표현된다.

π*(y|x)πref(y|x)exp!left(r(x,y)βright)

말로 풀면 이렇다. 최적 정책은 참조 정책에 보상의 지수를 곱한 것에 비례한다. 보상이 높은 응답의 확률이 그만큼 커진다.

DPO의 착상은 이 식을 뒤집는 것이다. 최적 정책이 보상으로 표현된다면, 보상도 정책으로 표현될 수 있다.

r(x,y)=βlogπ*(y|x)πref(y|x)+const

이제 이 표현을 세션 78의 보상 모델 손실에 대입한다. 그러면 r이 사라지고 정책만 남는다.

LDPO=logσ!left(βlogπ(yw|x)πref(yw|x)βlogπ(yl|x)πref(yl|x)right)

무엇이 없어졌는가

이 식이 실무에서 뜻하는 바는 이렇다.

그 결과 DPO는 SFT와 거의 같은 난이도가 되었다. 이것이 빠르게 표준이 된 이유다.

식을 말로 읽기

손실 안의 각 항이 무엇인지 보면 동작이 분명해진다.

logπ(yw|x)/πref(yw|x)선택된 응답에 대해 현재 모델이 참조 모델보다 얼마나 더 높은 확률을 주는가다. 이것이 커지길 원한다.

logπ(yl|x)/πref(yl|x)는 거부된 응답에 대한 같은 값이다. 이것이 작아지길 원한다.

둘의 차이를 로지스틱에 넣으므로, 손실을 줄이려면 선택된 쪽은 참조보다 올리고 거부된 쪽은 참조보다 내려야 한다. 참조 모델이 기준선 역할을 하면서 자연스럽게 KL 제약이 들어간다.

β는 여기서도 같은 역할이다. 크면 참조에서 덜 벗어나고, 작으면 더 크게 변한다.

확인 질문

  1. DPO가 보상 모델을 소거할 수 있는 근거가 되는 사실은 무엇인가?
  2. DPO 손실 안의 두 로그비가 각각 무엇을 뜻하는가?
  3. DPO에서 KL 제약은 어디에 들어 있는가?
확인

1. KL 제약이 붙은 보상 최대화 문제의 최적 정책이 닫힌 형태로 존재하고, 그것을 뒤집으면 보상을 정책으로 표현할 수 있다는 사실이다. 그 표현을 보상 모델 손실에 대입하면 보상이 소거된다.

2. 선택된 응답과 거부된 응답 각각에 대해, 현재 모델이 참조 모델보다 얼마나 높은 확률을 주는지다. 앞은 커지고 뒤는 작아지도록 학습된다.

3. 참조 모델과의 로그확률 비 안에 들어 있다. 참조 모델이 기준선이 되어 그로부터의 이탈이 손실에 자연스럽게 반영된다.

앉아서 읽을 것

DPO 논문의 4절이 위 유도 과정이다. 세 쪽 남짓이고, 이 세션을 읽은 뒤라면 따라갈 수 있다.


Session 82DPO의 한계

DPO는 단순해서 널리 쓰이지만 실전에서 세 가지 문제가 반복적으로 확인되었다. 길이 편향, 참조 모델 의존, 선호쌍 구축 비용이다.

길이 편향

DPO 손실은 응답 전체의 로그확률을 쓴다. 그런데 로그확률은 토큰마다의 값을 더한 것이므로 응답이 길수록 절댓값이 커진다.

이 성질이 학습에 왜곡을 만든다. 긴 응답과 짧은 응답을 비교할 때 길이 자체가 신호에 섞여 들어가고, 그 결과 모델이 이유 없이 길게 쓰는 방향으로 밀린다. 세션 78에서 본 사람의 길이 선호와 겹쳐서 효과가 증폭된다.

대응책으로 길이로 정규화하는 변형들이 제안되었다. 로그확률을 토큰 수로 나누거나, 참조 모델을 아예 쓰지 않는 방식 등이다. 이름은 여럿이지만 문제의식은 같다.

참조 모델 의존

DPO는 참조 모델의 로그확률을 매 스텝 계산해야 한다. 참조 모델은 학습되지 않지만 메모리에는 올라가 있어야 하고, 순전파도 매번 해야 한다. PPO보다는 훨씬 가볍지만 SFT보다는 두 배 가깝게 무겁다.

미리 계산해 두는 방법이 있다. 참조 모델은 변하지 않으므로 데이터셋의 모든 응답에 대한 로그확률을 학습 전에 한 번 계산해 저장하면, 학습 중에는 참조 모델을 메모리에서 뺄 수 있다. 데이터셋이 고정되어 있을 때 쓸 수 있는 실용적인 최적화다.

더 근본적인 문제는 참조 모델이 무엇이냐에 따라 결과가 달라진다는 점이다. SFT를 잘 마친 모델을 참조로 쓰면 좋지만, 참조 모델이 나쁘면 그 기준선 자체가 나쁘다.

선호쌍 구축 비용

이것이 가장 실질적인 장벽이다.

DPO는 (프롬프트, 선택, 거부) 삼중항을 요구한다. 이 데이터를 만들려면 같은 프롬프트에 여러 응답을 생성하고 사람이 비교해야 한다. 수만 건 규모로 만들면 비용이 상당하다.

그리고 데이터의 분포가 중요하다. 학습 대상 모델이 실제로 생성할 법한 응답들을 비교해야 의미가 있는데, 다른 모델이 만든 응답으로 만든 데이터를 쓰면 효과가 떨어진다. 이것을 오프폴리시 문제라고 부르고, 강화학습 계열이 이 점에서는 유리하다. 학습 중에 현재 정책이 만든 응답으로 즉시 학습하기 때문이다.

그래서 언제 쓰는가

정리하면 DPO는 이런 조건에서 좋은 선택이다. 선호 데이터를 이미 확보했거나 만들 수 있고, 응답 길이를 통제할 수단이 있으며, 파이프라인을 단순하게 유지하고 싶을 때다.

반대로 검증 가능한 정답이 있는 과제라면 다음 세션들의 방법이 낫다. 선호쌍을 만들 필요 없이 자동 채점이 가능하기 때문이다.

확인 질문

  1. DPO의 길이 편향이 생기는 이유를 로그확률의 성질로 설명해 보라.
  2. 참조 모델의 로그확률을 미리 계산해 두는 최적화가 가능한 이유는?
  3. 오프폴리시 문제란 무엇이며 왜 DPO에 불리한가?
확인

1. 응답의 로그확률은 토큰별 로그확률의 합이므로 길이가 길수록 절댓값이 커진다. 그 크기 차이가 학습 신호에 섞여 들어가 길이 자체가 선호처럼 작동한다.

2. 참조 모델은 학습 중에 변하지 않기 때문이다. 데이터셋이 고정이면 모든 응답의 로그확률을 미리 한 번 계산해 저장하고, 학습 중에는 참조 모델을 메모리에서 뺄 수 있다.

3. 학습 대상 모델이 아닌 다른 분포에서 생성된 응답으로 학습하는 상황이다. DPO는 고정된 데이터셋을 쓰므로 학습이 진행되며 정책이 변해도 데이터는 그대로라, 현재 정책이 실제로 만드는 응답과 괴리가 생긴다.


Session 83GRPO

가치 모델을 없애고, 같은 프롬프트에 여러 응답을 뽑아 서로 비교하는 것으로 대신한다. 그룹 평균보다 나은지 나쁜지가 학습 신호가 되고, 이 단순화가 2026년 현재 표준의 자리를 만들었다.

무엇을 없앴는가

세션 79에서 PPO가 가치 모델을 필요로 한다고 했다. 어떤 상태에서 앞으로 받을 보상의 기댓값을 추정해서, 실제 보상이 그보다 나은지 판단하는 기준선 역할이다.

가치 모델은 정책과 비슷한 크기라 메모리를 많이 쓰고, 그것 자체를 학습시켜야 해서 불안정 요소가 하나 늘어난다.

GRPO의 착상은 이렇다. 기준선을 모델로 추정하지 말고, 같은 프롬프트에 여러 응답을 뽑아 그 평균을 기준선으로 쓰자.

절차

프롬프트 하나에 대해 응답을 G개 생성한다. 보통 4에서 16개다. 각각에 보상을 매긴다. 그다음 그룹 안에서 정규화한다.

Ai=rimean(r1,,rG)std(r1,,rG)

이 값이 advantage, 즉 "이 응답이 같은 프롬프트의 다른 시도들보다 얼마나 나은가"다. 평균보다 좋으면 양수라 그 방향으로 확률을 올리고, 나쁘면 음수라 내린다.

여기에 PPO와 마찬가지로 갱신 폭 제한과 KL 항이 붙는다.

왜 잘 작동하는가

대가

공짜는 아니다. 프롬프트마다 응답을 G개 생성해야 하므로 생성 비용이 G다. M7에서 볼 대로 생성은 순차적이라 느리고, 이것이 GRPO 학습의 주된 시간 소모 지점이다.

그래서 GRPO 학습 파이프라인은 생성을 효율적으로 하는 것이 중요해진다. M9에서 볼 서빙 엔진을 학습 루프 안에 넣어 배치 생성을 빠르게 하는 구조가 흔히 쓰인다. 학습과 서빙이 만나는 지점이 여기다.

확인 질문

  1. GRPO가 가치 모델을 없앨 수 있는 원리를 설명해 보라.
  2. advantage를 그룹 안에서 정규화하는 이유는?
  3. GRPO의 주된 비용은 어디서 발생하는가?
확인

1. 같은 프롬프트에 여러 응답을 생성해 그 보상들의 평균을 기준선으로 쓰기 때문이다. 모델로 추정할 필요 없이 실제 시도들의 평균이 그 프롬프트의 정확한 기준선이 된다.

2. 프롬프트마다 보상의 절대 수준이 다르기 때문이다. 그룹 안에서 평균을 빼고 표준편차로 나누면 "같은 조건의 다른 시도들보다 나은가"라는 상대적 신호만 남는다.

3. 프롬프트마다 응답을 여러 개 생성해야 하므로 생성 비용이 그만큼 배가된다. 생성은 순차적이라 느려서 학습 시간의 대부분을 차지한다.


Session 84RLVR — 검증 가능한 보상

사람의 선호 대신 프로그램이 정답을 판정한다. 보상 모델이 없으므로 허점도 없고, 노이즈도 없다. 수학·코드·형식 준수처럼 채점 가능한 영역에서 최근 가장 큰 진전이 여기서 나왔다.

발상

지금까지의 흐름을 되짚어 보자. 사람이 채점하기엔 느려서 보상 모델을 만들었고, 보상 모델은 근사라 허점이 있어서 KL 제약이 필요했다.

그런데 어떤 과제에서는 정답을 프로그램으로 확인할 수 있다.

수학 문제라면 최종 답이 맞는지 비교하면 된다. 코드라면 테스트를 돌리면 된다. JSON 형식을 지켰는지는 파싱해 보면 된다. 특정 함수를 호출했는지도 확인할 수 있다.

이 경우 보상은 검증기의 출력이다. 맞으면 1, 틀리면 0. 근사가 아니라 정확한 값이다.

무엇이 좋아지는가

무엇이 관측되었는가

RLVR을 적용한 모델들에서 흥미로운 현상이 보고되었다. 정답률만 보상했는데 추론 과정 자체가 길어지고 정교해졌다. 스스로 검산하거나, 접근을 바꿔 다시 시도하거나, 중간에 오류를 발견하고 수정하는 행동이 나타났다.

이것은 M0 세션 2에서 본 논리의 반복이다. 아무도 검산하라고 가르치지 않았지만, 검산하면 정답률이 오르기 때문에 학습된 것이다. 목표를 밀어붙이면 수단이 획득된다.

한계

2026년의 표준 레시피

현재 널리 쓰이는 파이프라인은 3단계다.

이 순서에는 이유가 있다. 초기 모델이 형식조차 못 지키면 검증기가 항상 0을 주므로 학습 신호가 없다. SFT가 먼저 그 바닥을 깔아 줘야 RLVR이 작동한다.

확인 질문

  1. RLVR에서 reward hacking이 원천적으로 불가능한 이유는?
  2. 정답률만 보상했는데 추론 과정이 정교해지는 현상을 M0의 어떤 논리로 설명할 수 있는가?
  3. 2026년 표준 레시피의 3단계와, SFT가 먼저여야 하는 이유는?
확인

1. 보상이 근사 모델이 아니라 정확한 검증기의 출력이기 때문이다. 파고들 빈틈이 없고, 답이 맞아야만 보상을 받는다. (단, 검증기 자체가 허술하면 그 빈틈은 여전히 남는다.)

2. 목표를 극단적으로 밀어붙이면 그 목표를 달성하는 수단이 학습된다는 논리다. 검산하면 정답률이 오르므로 검산하는 행동이 획득된다.

3. SFT → 선호 최적화 → RLVR이다. 초기 모델이 형식조차 못 지키면 검증기가 항상 0을 줘서 학습 신호가 생기지 않으므로, SFT가 먼저 바닥을 깔아 줘야 한다.


Session 85LoRA

가중치를 통째로 학습시키지 않고 변화량만 두 개의 얇은 행렬로 학습시킨다. 절약되는 것은 파라미터 수가 아니라 optimizer state와 gradient이고, 이 구분이 LoRA를 정확히 이해하는 열쇠다.

구조

원래 가중치 W를 얼려 두고, 그 옆에 두 개의 작은 행렬을 붙인다.

h=Wx+αrBAx

A[r, d], B[d, r]이고 r은 rank로 보통 4에서 64 사이다. M1 세션 10에서 본 저랭크 분해가 여기 그대로 쓰인다.

학습되는 것은 AB뿐이다. W는 고정이다.

초기화가 중요하다. A는 무작위로, B0으로 초기화한다. 그러면 학습 시작 시점에 BA=0이므로 모델이 원래 모델과 정확히 같다. 여기서 출발해 조금씩 벗어나는 구조가 안정적이다. 둘 다 무작위로 초기화하면 시작부터 모델이 망가진다.

α/r 스케일링은 rank를 바꿔도 학습 강도가 비슷하게 유지되도록 하는 보정이다. 이것을 빼먹으면 rank마다 사실상 다른 학습률을 쓰는 셈이 된다.

무엇이 절약되는가

이 세션의 핵심이다. M2 세션 27에서 본 메모리 회계를 다시 꺼내 보자.

전체 파인튜닝에서 파라미터 P개에 필요한 것은 이렇다. 가중치 P, gradient P, optimizer state 2P. 여기에 정밀도까지 고려하면 파라미터당 대략 16바이트가 든다. 8B 모델이면 128GB에 가깝다.

LoRA에서 학습 대상은 AB뿐이다. 전체의 0.1퍼센트 수준이라고 하면, gradient와 optimizer state도 그 0.1퍼센트만 있으면 된다. 원래 가중치는 얼려 있으므로 gradient도 optimizer state도 필요 없다.

파인튜닝 메모리의 구성

8B 모델 기준의 대략적인 비교다. 가중치 자체는 어느 쪽이든 메모리에 있어야 한다. 사라지는 것은 기울기와 옵티마이저 상태이고, 그 둘이 전체 파인튜닝 메모리의 4분의 3을 차지한다. LoRA가 "80GB 한 장으로 8B를 파인튜닝할 수 있게" 만드는 지점이 정확히 여기다.

merge

학습이 끝나면 BA를 계산해 W에 더할 수 있다.

W'=W+αrBA

이렇게 하면 원래 모델과 구조가 완전히 같아지고 추론 오버헤드가 0이 된다. 이것이 LoRA의 큰 장점이다. 어댑터를 붙인 채로 추론하면 행렬곱이 두 번 더 들어가지만, merge하면 그것이 사라진다.

대가는 유연성이다. merge하면 어댑터를 바꿀 수 없다. 여러 고객마다 다른 어댑터를 서비스해야 한다면 merge하지 않고 붙인 채로 서빙해야 한다. M9에서 볼 멀티 LoRA 서빙이 이 경우다.

어디에 붙이는가

모든 선형 계층에 붙일 필요는 없다. 초기 연구는 attention의 Q와 V에만 붙였고, 이후에는 attention의 네 행렬 전부, 나아가 FFN까지 붙이는 것이 성능이 낫다는 결과가 나왔다.

FFN이 파라미터의 3분의 2라는 M4 세션 55의 사실을 떠올리면, FFN에 붙이지 않으면 모델의 대부분을 손대지 않는 셈이다. 적응 폭이 커야 하는 과제일수록 FFN을 포함하는 것이 중요해진다.

확인 질문

  1. B를 0으로 초기화하는 이유는?
  2. LoRA가 절약하는 것은 정확히 무엇인가? 가중치 메모리도 줄어드는가?
  3. merge의 장점과 그 대가는?
확인

1. 학습 시작 시점에 BA=0이 되어 모델이 원래 모델과 정확히 같아지기 때문이다. 원래 성능에서 출발해 조금씩 벗어나는 구조라 안정적이다.

2. gradient와 optimizer state를 절약한다. 원래 가중치는 얼려 있어도 메모리에는 그대로 있어야 하므로 가중치 메모리는 줄지 않는다.

3. 장점은 추론 오버헤드가 0이 된다는 것이다. 대가는 어댑터를 교체할 수 없게 되어 여러 어댑터를 동시에 서비스할 수 없다는 것이다.

앉아서 읽을 것

LoRA 논문은 짧고 읽기 쉽다. 특히 어떤 층에 붙일지, rank를 얼마로 할지에 대한 실험 표가 실무에 바로 쓰인다.


Session 86QLoRA와 어댑터 운용

베이스 모델을 4비트로 양자화해 얼려 두고 그 위에 LoRA를 얹으면, 가중치 메모리마저 4분의 1로 줄어든다. 앞 세션에서 남아 있던 마지막 덩어리가 여기서 처리된다.

남은 문제

LoRA는 gradient와 optimizer state를 없앴지만 가중치 자체는 그대로 남는다. 8B 모델을 bf16으로 올리면 16GB다. 70B면 140GB로 단일 GPU에 들어가지 않는다.

그런데 관찰해 보면 이 가중치는 학습되지 않는다. 순전파와 역전파에서 읽기만 할 뿐 갱신되지 않는다. 그렇다면 정밀도를 낮춰도 되지 않을까.

세 가지 기법

QLoRA는 세 가지를 결합했다.

이 조합으로 70B 모델을 GPU 한 장에서 파인튜닝할 수 있게 되었다.

대가

그래서 선택 기준은 명확하다. 메모리가 부족하면 QLoRA, 여유가 있으면 일반 LoRA다.

어댑터 운용

LoRA 어댑터는 작다. 수십에서 수백 MB 수준이라 여러 개를 만들어 관리하기 쉽다. 여기서 실무적으로 유용한 패턴들이 나온다.

M9에서 볼 서빙 엔진들은 이 패턴을 지원한다. 베이스 모델을 한 번만 올리고 여러 어댑터를 동적으로 적용해, 요청마다 다른 어댑터로 응답할 수 있다.

확인 질문

  1. 베이스 모델을 4비트로 양자화해도 되는 근거는?
  2. QLoRA가 일반 LoRA보다 느린 이유는?
  3. 어댑터를 merge하지 않고 운용할 때 얻는 이점은?
확인

1. 베이스 가중치가 학습되지 않고 읽기만 되기 때문이다. 갱신이 없으므로 정밀도를 낮춰도 학습 자체가 망가지지 않는다.

2. 4비트로 저장된 가중치를 계산할 때마다 원래 정밀도로 풀어야 하고, 그 역양자화 비용이 매 순전파·역전파마다 발생한다.

3. 베이스 모델 하나를 공유하면서 고객별·과제별로 다른 어댑터를 붙일 수 있다. 어댑터가 작아 버전 관리와 롤백도 쉽다.


Session 87무엇을 언제 고르는가

이 모듈의 결론이다. 프롬프트, RAG, SFT, LoRA, 선호 최적화, RLVR 중 무엇을 고를 것인가. 판단의 축은 무엇을 바꾸려 하는가검증할 수 있는가 두 개다.

첫 번째 질문 — 무엇을 바꾸려 하는가

이 질문에 정확히 답하면 절반은 끝난다.

두 번째 질문 — 검증할 수 있는가

정답을 프로그램으로 판정할 수 있으면 선택지가 넓어진다. 데이터를 자동 생성할 수 있고, 보상 모델 없이 강화학습이 가능하고, 평가도 자동화된다.

검증할 수 없으면 사람이 개입해야 하고, 그 비용이 모든 결정을 지배한다. 이때는 가능한 한 위층에서 푸는 것이 옳다.

판단표

상황첫 선택이유
사내 문서 내용을 물어봄RAG지식이 자주 바뀌고 출처가 필요하다
응답을 항상 JSON으로제약 디코딩학습이 아니라 디코딩 계층에서 보장된다
회사 말투와 응답 구조SFT (LoRA로)형식은 SFT가 가장 잘 가르친다
법률 문서 문체가 낯섦계속학습 → SFT어휘와 문체는 사전학습 계열의 문제다
두 답 중 어느 쪽이 나은지DPO본보기로는 가르칠 수 없는 영역이다
수학·코드 정확도RLVR (GRPO)검증기가 있으면 보상이 정확하다
무엇이 문제인지 모름평가부터M12를 먼저 하는 것이 맞다

마지막 줄이 실무에서 가장 자주 해당된다. 무엇이 문제인지 측정하지 않은 채 파인튜닝을 시작하는 것이 이 분야에서 가장 흔한 낭비다.

비용 순서

M5 세션 69의 표를 여기서 다시 쓴다. 위에서 아래로 갈수록 비싸다.

프롬프트 개선 → 제약 디코딩 → RAG → LoRA SFT → 전체 SFT → DPO → RLVR → 계속학습 → 사전학습

확인 질문

  1. "모델이 사내 규정을 모른다"에 파인튜닝이 나쁜 선택인 이유 두 가지는?
  2. 응답 형식을 강제하는 데 SFT보다 확실한 방법은 무엇이며 왜 그런가?
  3. 파인튜닝을 시작하기 전에 반드시 해야 할 일은?
확인

1. 첫째, 규정이 바뀌면 재학습해야 한다. 둘째, 어느 문서에서 나온 답인지 출처를 밝힐 수 없다. 지식은 파라미터가 아니라 컨텍스트에 두는 편이 맞다.

2. 제약 디코딩이다. 문법에 맞지 않는 토큰을 아예 고를 수 없게 막으므로 형식 위반이 구조적으로 불가능해진다. SFT는 확률을 높일 뿐 보장하지 않는다.

3. 무엇이 문제인지 측정하는 것이다. 평가 체계 없이 파인튜닝을 시작하면 개선 여부를 알 수 없고, 애초에 잘못된 층에서 문제를 풀고 있을 수 있다.


Session 88복습 — 사후학습의 지형

다섯 가지 방법이 나왔다. 각각이 무엇을 없애서 이전 것보다 단순해졌는지를 이어 말할 수 있으면 이 모듈은 끝난 것이다.

계보로 이어 보기

베이스 모델은 텍스트를 이어 쓸 뿐 질문에 답하지 않는다. SFT가 지시-응답 쌍으로 응답하는 행동, 멈추는 행동, 역할 구분을 가르친다. 목적함수는 사전학습과 같고 데이터와 마스킹만 다르다. 프롬프트 구간에 손실을 걸면 질문 생성법을 배우므로 응답 구간만 쓴다.

SFT는 본보기 하나를 흉내 내게 할 뿐 무엇이 더 나은지는 가르치지 못한다. 그래서 비교 데이터가 필요하다. RLHF는 그 비교로 보상 모델을 학습시키고, 강화학습으로 정책을 최적화한다. 모델이 네 개 필요하고 하이퍼파라미터가 많아 무겁다. 그리고 보상 모델은 근사라 허점이 있어서 KL 제약으로 원래 모델에서 멀어지는 것에 벌점을 물려야 한다.

이 모든 것에 LoRA를 얹을 수 있다. 학습 대상 파라미터를 줄여 gradient와 optimizer state를 아끼고, QLoRA는 베이스 가중치까지 4비트로 눌러 마지막 덩어리를 처리한다.

자가 점검

  1. 베이스 모델에 없는 능력을 SFT로 만들 수 없는 이유는? (S73)
  2. SFT 데이터에 모델이 모르는 사실의 정답을 넣으면 왜 환각이 늘어나는가? (S74)
  3. loss masking을 빼먹으면 나타나는 증상 두 가지는? (S75)
  4. PPO 기반 RLHF에서 메모리에 올라가는 모델 넷과, DPO·GRPO가 각각 무엇을 없앴는지 말해 보라. (S77, S81, S83)
  5. reward hacking이 일어나는 메커니즘과 KL 제약이 그것을 막는 원리는? (S80)
  6. DPO의 길이 편향이 생기는 수학적 이유는? (S82)
  7. RLVR이 안전한 이유와 적용 범위의 한계는? (S84)
  8. LoRA가 절약하는 것과 절약하지 못하는 것을 구분해 말해 보라. (S85)
  9. 학습 중 보상 점수만 보면 안 되는 이유는? (S80)
  10. "사내 문서를 모른다"와 "말투가 다르다"에 각각 무엇을 고를 것인가? (S87)
확인

1. 사후학습은 이미 있는 능력을 꺼내 쓰기 쉬운 형태로 포장하는 일이지 새 능력을 만드는 것이 아니기 때문이다. 사전학습에 없던 것은 여기서 생기지 않는다.

2. 모델이 그 사실을 배우는 것이 아니라 "이런 질문에는 자신 있게 구체적으로 답한다"는 태도를 배우기 때문이다.

3. 모델이 답한 뒤 다음 질문을 스스로 지어내는 증상, 그리고 학습 용량이 질문 생성 능력에 낭비되는 것이다. 종료 토큰까지 마스킹하면 멈추지 않는 증상도 나타난다.

4. 정책, 참조, 보상, 가치 모델이다. DPO는 보상 모델과 가치 모델을 함께 없앴고(강화학습 루프 자체를 제거), GRPO는 가치 모델을 없애고 그룹 평균으로 대체했다.

5. 보상 모델은 근사라 실제 품질과 어긋나는 영역이 있고, 최적화가 그 영역을 적극적으로 찾아낸다. 그런 영역은 대개 원래 모델의 분포에서 멀리 있으므로, 이탈 자체에 벌점을 물리면 접근이 억제된다.

6. 응답의 로그확률이 토큰별 값의 합이라 길수록 절댓값이 커지고, 그 크기 차이가 학습 신호에 섞여 길이 자체가 선호처럼 작동한다.

7. 보상이 근사 모델이 아니라 정확한 검증기의 출력이라 파고들 빈틈이 없다. 한계는 검증기를 만들 수 있는 과제에만 쓸 수 있다는 것이고, 검증기가 허술하면 그 빈틈은 여전히 노출된다.

8. gradient와 optimizer state를 절약한다. 베이스 가중치는 얼려 있어도 메모리에 그대로 있어야 하므로 절약되지 않는다. 그 부분은 QLoRA의 4비트 양자화가 처리한다.

9. hacking이 일어나도 보상 점수는 계속 올라가기 때문이다. 보상과 무관한 별도 평가셋을 함께 봐야 붕괴를 감지한다.

10. 앞은 RAG다. 지식이 자주 바뀌고 출처가 필요하다. 뒤는 SFT다. 형식과 말투는 SFT가 가장 잘 가르치는 영역이다.

모듈을 마치며

여기까지가 모델이 만들어지는 이야기의 끝이다. M5에서 능력이 정해지고 M6에서 행동이 정해졌다.

다음 모듈부터는 그 모델을 돌리는 이야기다. 그리고 여기서부터가 백엔드 엔지니어에게 가장 익숙하면서 동시에 가장 즉시 쓸모 있는 영역이다. 프롬프트를 넣으면 토큰이 나올 때까지 정확히 무슨 일이 벌어지는지, 왜 느린지, 어떻게 빠르게 하는지를 M7부터 M9까지 서른아홉 번의 이동에 걸쳐 본다.

M7세션 89–10214개

추론 메커니즘

프롬프트를 넣으면 토큰이 나올 때까지 정확히 무슨 일이 벌어지는가. 여기서부터가 백엔드 엔지니어에게 가장 즉시 쓸모 있는 영역이다.

Session 89프롬프트를 넣으면 무슨 일이 일어나는가

토큰화 → prefilldecode 루프 → 디토큰화. 네 단계이고, 그중 두 번째와 세 번째는 성격이 완전히 달라서 최적화 방향도 반대다.

전 과정

사용자가 문장을 보낸다. 서버가 하는 일을 순서대로 따라가 보자.

두 개의 서로 다른 시계

여기서 이 모듈 전체의 뼈대가 되는 관찰이 나온다. prefill과 decode는 성격이 완전히 다르다.

prefill은 한 번에 수천 개의 토큰을 처리한다. 큰 행렬곱이 여러 번 일어나고, GPU의 연산 능력을 꽉 채워 쓴다.

decode는 매번 토큰 하나를 처리한다. 행렬곱의 한쪽이 길이 1이라 아주 작다. 그런데 계산을 위해 모델 가중치 전체를 읽어야 한다. 즉 읽는 양은 그대로인데 계산하는 양은 작다.

이 차이가 서빙 지표를 둘로 나눈다. 첫 토큰이 나오기까지의 시간(TTFT)은 prefill이 지배하고, 이후 토큰 사이의 간격(TPOT)은 decode가 지배한다. 그리고 두 지표를 개선하는 수단이 서로 다르다. M9에서 이 이야기를 본격적으로 다룬다.

확인 질문

  1. 추론의 네 단계를 순서대로 말해 보라.
  2. prefill과 decode가 처리하는 토큰 수는 각각 몇 개인가?
  3. TTFT와 TPOT는 각각 어느 단계가 지배하는가?
확인

1. 토큰화, prefill, decode 루프, 디토큰화다.

2. prefill은 프롬프트 전체를 한 번에 처리하므로 수천 개일 수 있고, decode는 매 스텝 하나씩 처리한다.

3. TTFT는 prefill이, TPOT는 decode가 지배한다.


Session 90prefill과 decode

prefill은 연산에 묶여 있고 decode는 메모리 대역폭에 묶여 있다. 같은 모델의 같은 forward인데 병목이 정반대인 이유를 이해하면 서빙 최적화의 절반이 풀린다.

연산량 비교

프롬프트가 2000토큰이고 응답이 300토큰이라고 하자.

prefill은 2000토큰을 한 번에 처리한다. 파라미터당 2 FLOPs라는 M1 세션 9의 규칙을 쓰면, 8B 모델에서 대략 2×8e9×2000=3.2e13 FLOPs다.

decode는 스텝마다 1토큰이므로 2×8e9×1=1.6e10 FLOPs다. 300스텝이면 4.8e12다.

총 연산량은 prefill 쪽이 몇 배 크다. 그런데 실제로 걸리는 시간은 decode 쪽이 훨씬 길다. 왜 그런가.

읽는 양

이유는 연산이 아니라 데이터 이동에 있다.

decode 한 스텝에서 계산에 필요한 모델 가중치는 전부다. 8B 모델을 bf16으로 두면 16GB를 HBM에서 읽어야 한다. 토큰 하나를 만들기 위해서다.

H100의 메모리 대역폭이 대략 3.35 TB/s라고 하면, 16GB를 읽는 데 최소 16/33504.8ms가 걸린다. 연산 1.6e10 FLOPs는 같은 GPU에서 0.04ms면 끝난다. 100배 이상의 차이다.

즉 decode 스텝의 시간은 거의 전적으로 "가중치를 읽는 시간"이고, 연산은 그 사이에 묻힌다. 이것을 메모리 바운드라고 부른다.

prefill은 다르다. 가중치를 한 번 읽어서 2000개 토큰에 대해 계산하므로, 읽기 비용이 2000개에 분산 상환된다. 연산이 지배적이 되고 GPU 사용률이 높다.

연산 강도

이 관계를 하나의 숫자로 표현하는 것이 연산 강도다. 읽은 바이트당 몇 번의 연산을 하는가.

decode 배치 1이면 16GB를 읽어 1.6e10 FLOPs를 하므로 대략 1 FLOP/byte다. prefill은 같은 16GB로 3.2e13을 하므로 2000 FLOP/byte다.

현대 GPU가 대역폭에 막히지 않으려면 대략 200에서 400 FLOP/byte가 필요하다. decode는 그 근처에도 못 간다. 이 사실이 M8의 roofline 이야기로 이어진다.

탈출구는 배치뿐

decode를 빠르게 하는 방법을 생각해 보자. 계산을 줄여도 소용없다. 시간을 쓰는 것은 계산이 아니다.

읽는 양을 줄이는 것은 방법이다. 양자화로 가중치를 4비트로 만들면 읽을 바이트가 4분의 1이 되고, 그만큼 빨라진다. 이것이 M8에서 양자화가 decode에 특히 효과적인 이유다.

그런데 가장 강력한 방법은 배치를 키우는 것이다. 가중치 16GB를 한 번 읽어서 요청 하나를 처리하든 서른 개를 처리하든 읽는 양은 같다. 배치가 커지면 읽기 비용이 분산 상환되어 연산 강도가 올라간다.

이것이 M9의 continuous batching이 존재하는 근본 이유다. 처리량을 올리는 유일한 실질적 수단이 동시 처리 요청 수를 늘리는 것이기 때문이다.

확인 질문

  1. decode의 총 연산량이 prefill보다 작은데도 시간이 더 걸리는 이유는?
  2. 연산 강도가 무엇이며 decode와 prefill에서 각각 대략 얼마인가?
  3. decode를 빠르게 하는 두 가지 수단을 말해 보라.
확인

1. decode는 토큰 하나를 만들기 위해 모델 가중치 전체를 HBM에서 읽어야 하고, 그 읽기 시간이 연산 시간을 압도한다. 스텝 수만큼 이 읽기가 반복된다.

2. 읽은 바이트당 수행하는 연산 횟수다. decode 배치 1은 약 1 FLOP/byte, prefill은 프롬프트 길이만큼 커서 수천 FLOP/byte에 이른다.

3. 읽을 바이트를 줄이는 양자화와, 한 번 읽은 가중치로 더 많은 요청을 처리하는 배칭이다.


Session 91autoregressive decoding

토큰을 하나 만들면 그것이 다음 토큰의 입력이 된다. 이 의존성 때문에 생성은 원리적으로 병렬화할 수 없고, 그것이 LLM 서빙의 모든 어려움의 뿌리다.

순차성의 근원

M1 세션 11에서 본 자기회귀 구조를 다시 보자.

P(y1,,yn|x)=t=1nP(yt|x,y<t)

y_t를 계산하려면 y_{t-1}이 필요하고, 그것을 계산하려면 y_{t-2}가 필요하다. 이 사슬은 끊을 수 없다.

학습에서는 정답을 이미 알고 있으므로 모든 위치를 한 번에 계산할 수 있었다. M4 세션 50의 causal mask가 그것을 가능하게 했다. 그런데 생성에서는 정답이 없다. 만들어 가면서 알아낸다.

결과

순차성을 우회하려는 시도들

이 제약을 완화하려는 여러 방향이 있다.

정리하면, 지연을 줄이는 것보다 처리량을 올리는 것이 훨씬 쉽다. 이 비대칭이 LLM 서빙 설계의 기본 조건이다.

확인 질문

  1. 학습에서는 병렬 계산이 가능한데 생성에서는 불가능한 이유는?
  2. 모델을 최적화해도 응답 시간을 줄이는 데 한계가 있는 이유는?
  3. 배칭이 개선하는 것은 지연인가 처리량인가?
확인

1. 학습에서는 정답 토큰을 이미 알고 있어 모든 위치의 입력이 준비되어 있지만, 생성에서는 앞 토큰을 만들어야 다음 입력이 정해지기 때문이다.

2. 응답 길이만큼의 순차 스텝이 필요하고 그 스텝 수는 최적화로 줄지 않기 때문이다. 스텝당 시간을 줄일 수는 있어도 스텝 수는 응답 길이가 정한다.

3. 처리량이다. 개별 요청의 응답 시간은 오히려 조금 늘어날 수 있다.


Session 92KV cache — 무엇을 저장하는가

각 층에서 계산된 K와 V를 보관한다. Q는 저장하지 않는다. 이 비대칭이 어디서 오는지가 이 세션의 전부이고, 알고 나면 KV cache의 크기 공식도 자연스럽게 따라온다.

재계산의 낭비

decode 루프의 두 번째 스텝을 생각해 보자. 지금까지 토큰이 2001개(프롬프트 2000 + 생성 1)이고, 2002번째를 만들어야 한다.

순진하게 하면 2001개 토큰 전체를 다시 모델에 넣는다. 그런데 앞의 2000개는 이전 스텝에서 이미 계산했다. 입력이 같고 파라미터도 같으므로 결과도 같다. 완전히 같은 계산을 반복하는 것이다.

세 번째 스텝에서는 2001개를 다시 계산한다. 이렇게 가면 총 계산량이 길이의 제곱에 비례한다.

무엇을 재사용할 수 있는가

M4 세션 51의 attention 계산을 새 토큰의 관점에서 보자. 새 토큰이 자기 표현을 만들려면 이것이 필요하다.

여기서 비대칭이 명확해진다. K와 V는 이전 토큰들의 것이 계속 필요하고, Q는 현재 토큰의 것만 필요하다. 그래서 K와 V만 저장한다.

어디에 저장하는가

층마다 따로 저장한다. 32층 모델이면 32세트의 K와 V가 있다. 각 층의 attention이 그 층의 K, V를 쓰기 때문이다.

그리고 헤드마다 따로다. GQA를 쓰면 K와 V의 헤드 수가 Q보다 적으므로 그만큼만 저장한다. M4 세션 60에서 GQA의 목적이 이것이라고 한 이유다.

무엇이 달라지는가

KV cache가 있으면 decode 스텝의 입력이 토큰 하나가 된다. 그 토큰의 Q, K, V를 계산하고, 새 K와 V를 캐시에 덧붙이고, 캐시 전체와 attention을 수행한다.

계산량이 극적으로 준다. 매 스텝 전체를 다시 계산하는 대신 새 토큰 하나분만 계산한다. 다음 세션에서 이 차이를 정확히 센다.

확인 질문

  1. Q를 캐시하지 않는 이유를 설명해 보라.
  2. KV cache는 몇 세트가 존재하는가?
  3. GQA가 KV cache 크기를 줄이는 원리는?
확인

1. 새 토큰이 attention을 계산할 때 필요한 것은 자기 자신의 Q뿐이다. 이전 토큰들의 Q는 그들의 attention 계산이 이미 끝났으므로 다시 쓰이지 않는다.

2. 층마다 한 세트씩, 그리고 각 층 안에서 헤드마다 따로 존재한다. 32층이면 32세트다.

3. 여러 Q 헤드가 K와 V를 공유하므로 저장할 K, V 헤드 수가 줄어든다. 32헤드를 8그룹으로 묶으면 4분의 1이 된다.


Session 93KV cache — 왜 빨라지는가

캐시가 없으면 누적 계산이 T(T+1)/2, 있으면 T다. 삼각형이 대각선이 된다. 길이가 길어질수록 이 차이가 벌어져, 긴 컨텍스트에서는 캐시 없이는 서비스가 불가능하다.

세어 보기

T개의 토큰을 생성한다고 하자.

T가 100이면 5050 대 100으로 50배 차이다. T가 1000이면 500500 대 1000으로 500배다. 길이에 비례해 격차가 커진다.

삼각형과 대각선

세로가 생성 스텝, 가로가 토큰 위치다. 진한 칸이 그 스텝에 실제로 계산한 것, 옅은 칸은 캐시에서 재사용한 것이다. 왼쪽은 매번 처음부터 다시 계산해 삼각형이 채워지고, 오른쪽은 대각선 한 줄만 남는다.

정확히 무엇이 줄어드는가

한 가지 짚어 둘 것이 있다. KV cache가 계산량을 완전히 선형으로 만들지는 못한다.

새 토큰의 attention은 캐시에 있는 모든 K와 내적해야 하므로, 그 부분은 여전히 t에 비례한다. 캐시가 없앤 것은 이전 토큰들의 QKV 투영과 FFN 계산이다. 이 부분이 계산량의 대부분이므로 실질적으로 큰 이득이지만, attention 자체는 스텝마다 조금씩 무거워진다.

그래서 아주 긴 컨텍스트에서는 캐시가 있어도 후반 스텝이 초반보다 느려진다. 이 현상은 실제 서빙에서 관측되고, 긴 대화에서 응답이 점점 느려지는 원인 중 하나다.

무엇을 대가로 치르는가

계산을 메모리와 맞바꾼 것이다. 캐시는 VRAM을 차지하고, 그 크기가 다음 세션의 주제다.

그리고 이 맞바꿈이 서빙 설계 전체를 규정한다. 캐시가 없으면 계산이 감당 안 되고, 캐시가 있으면 메모리가 감당 안 된다. 그 사이에서 균형을 잡는 것이 M9의 여러 기법들이다.

백엔드의 언어로

확인 질문

  1. 캐시 유무에 따른 누적 계산량을 각각 수식으로 써 보라.
  2. KV cache가 있어도 스텝이 완전히 일정한 시간이 되지 않는 이유는?
  3. KV cache가 일반적인 캐시와 다른 점 하나를 말해 보라.
확인

1. 캐시 없으면 T(T+1)/2, 있으면 T다.

2. 새 토큰의 attention이 캐시에 있는 모든 K와 내적해야 하므로 그 부분은 여전히 현재 길이에 비례한다. 캐시가 없앤 것은 이전 토큰들의 투영과 FFN 계산이다.

3. 캐시 미스가 없고 순차 축적된다는 점, 크기가 계속 자란다는 점, 그리고 버리면 그 요청을 처리할 수 없어 축출이 자유롭지 않다는 점이다.


Session 94KV cache 메모리 공식

이 공식 하나가 서비스 용량을 결정한다. 컨텍스트 길이와 동시 요청 수에 정비례하므로, 둘을 동시에 늘릴 수 없다는 제약이 여기서 나온다.

공식

KVbytes=2×L×Hkv×dh×T×B×bytes/elem

각 항이 무엇인지 짚어 보자. 맨 앞의 2는 K와 V 두 개다. L은 층 수, H_kvKV 헤드 수(GQA면 Q 헤드보다 적다), d_h는 헤드 차원, T는 시퀀스 길이, B는 동시 요청 수, 마지막은 정밀도다.

토큰당 크기부터

실무에서 쓰기 편한 형태는 토큰 하나당 몇 바이트인가다. 위 식에서 TB를 빼면 된다.

8B급 모델의 전형적인 설정으로 계산해 보자. L=32, Hkv=8, dh=128, bf16이면 2바이트다.

2×32×8×128×2=131072;bytes=128;KiB

만약 GQA를 쓰지 않고 Hkv=32였다면 512 KiB가 된다. 네 배다. M4 세션 60에서 GQA가 왜 중요한지 말한 것이 이 숫자다.

VRAM 예산 계산기

8k
8
8B 모델(bf16, GQA 8헤드) 기준이다. 파라미터 16GB는 고정이지만 KV 캐시는 컨텍스트와 동시성의 에 비례한다. 이 곱이 커지면 GPU 한 장으로는 감당이 안 되고, 그때 선택지는 동시성을 줄이거나 컨텍스트를 줄이거나 양자화하는 것뿐이다.

무엇을 함의하는가

어떻게 쓰는가

서빙 설정을 잡을 때 순서는 이렇다.

먼저 GPU 총 VRAM에서 모델 가중치를 뺀다. 남은 것이 KV cache와 활성값에 쓸 수 있는 예산이다. 실제로는 여유를 두므로 90% 정도만 쓴다.

그다음 목표 컨텍스트 길이를 정하고, 위 공식으로 요청 하나가 최대로 쓸 KV를 계산한다. 예산을 그 값으로 나누면 동시에 받을 수 있는 요청 수가 나온다.

이 계산이 M9 세션 122의 용량 산정으로 이어진다.

확인 질문

  1. KV cache 공식을 써 보고 각 항이 무엇인지 말해 보라.
  2. L=32,Hkv=8,dh=128, bf16 모델의 토큰당 KV 크기는?
  3. 80GB GPU에 8B 모델을 올리고 컨텍스트 16k를 지원할 때 동시 요청은 대략 몇 개까지 가능한가?
확인

1. 2×L×Hkv×dh×T×B×bytes. 2는 K와 V, L은 층 수, H_kv는 KV 헤드 수, d_h는 헤드 차원, T는 시퀀스 길이, B는 동시 요청 수다.

2. 2×32×8×128×2=131072 바이트, 즉 128 KiB다.

3. 가중치 16GB를 빼면 약 64GB, 여유를 두면 약 58GB다. 요청 하나가 16384×128KiB=2GiB를 쓰므로 대략 29개다.


Session 95왜 decode는 메모리 바운드인가

세션 90에서 결론만 말한 것을 여기서 확정한다. 그리고 이 사실이 양자화, 배칭, GQA, speculative decoding이 모두 같은 문제를 공격하고 있다는 통합된 그림을 준다.

시간이 어디로 가는가

decode 한 스텝에서 GPU가 하는 일을 나눠 보자.

H100 기준으로 읽기는 약 5ms, 연산은 약 0.02ms다. 250배 차이다. 시간은 전적으로 읽기가 쓴다.

통합된 그림

이 사실을 알면 여러 기법이 하나의 목표를 향한다는 것이 보인다.

기법무엇을 하는가왜 효과가 있는가
양자화가중치를 4비트로읽을 바이트가 4분의 1
GQAKV 헤드 공유KV cache 읽기가 줄어듦
배칭여러 요청 동시 처리한 번 읽어 여러 토큰 처리
speculative여러 토큰 한 번에 검증한 번 읽어 여러 스텝 진행
KV 양자화KV를 8비트로KV 읽기가 절반

이 관점은 실무 판단에도 쓰인다. 어떤 최적화 제안을 받았을 때 "이것이 읽는 바이트를 줄이는가, 아니면 한 번의 읽기로 처리하는 토큰 수를 늘리는가"를 물어보면 효과 여부를 대략 판단할 수 있다.

배치가 커지면 어떻게 되는가

배치를 계속 키우면 언젠가 연산이 병목이 된다. 가중치 읽기는 배치와 무관하게 일정한데 연산은 배치에 비례하기 때문이다.

전환점은 대략 연산 강도가 GPU의 임계값에 도달하는 지점이다. H100에서 bf16이면 대략 배치 200에서 400 근처다. 그 이전까지는 배치를 키워도 스텝 시간이 거의 늘지 않는다. 공짜로 처리량이 오르는 구간이다.

다만 실제로는 그 전에 KV cache 메모리가 먼저 한계에 닿는 경우가 많다. 앞 세션의 공식대로 배치가 커지면 KV도 비례해 커지기 때문이다. 그래서 실무의 배치 크기 상한은 대개 메모리가 정한다.

확인 질문

  1. decode 한 스텝에서 읽기와 연산의 시간 비율은 대략 얼마인가?
  2. 양자화가 decode를 빠르게 하는 이유를 한 문장으로 말해 보라.
  3. 배치를 키우면 스텝 시간이 거의 늘지 않는 구간이 있는 이유는?
확인

1. 8B 모델 기준으로 읽기가 약 5ms, 연산이 약 0.02ms로 200배 이상 차이가 난다.

2. decode의 시간은 가중치를 읽는 시간이 지배하므로, 가중치의 비트 수를 줄이면 읽을 바이트가 줄어 그만큼 빨라진다.

3. 가중치 읽기 시간은 배치와 무관하게 일정한데 그 사이에 처리하는 토큰 수만 늘어나기 때문이다. 연산이 병목이 되는 지점까지는 거의 공짜로 처리량이 오른다.


Session 96temperature

온도는 softmax 직전에 logits를 나누는 상수다. 모델의 계산을 바꾸지 않고 선택의 과감함만 조절한다. 이 구분이 온도를 이해하는 핵심이다.

무엇을 바꾸는가

M1 세션 14에서 이미 정의를 봤다.

pi=exp(zi/T)jexp(zj/T)

T로 나눈 뒤 softmax를 적용한다. T < 1이면 logit의 차이가 벌어져 분포가 뾰족해지고, T > 1이면 차이가 줄어 평평해진다.

여기서 중요한 것은 모델의 forward는 온도와 무관하다는 점이다. logits는 이미 나와 있고, 온도는 그것을 확률로 바꾸는 방식만 정한다. 즉 온도를 바꿔도 모델이 "아는 것"은 변하지 않고, 그중 무엇을 고를지의 성향만 바뀐다.

실무 감각

온도 0이 결정적이지 않은 이유

실무에서 자주 부딪히는 함정이다. 온도를 0으로 두면 같은 입력에 같은 출력이 나올 것 같지만 그렇지 않다.

이유는 여러 가지다. 첫째, 부동소수점 연산은 순서에 따라 결과가 미세하게 다르다. 배치 구성이 달라지면 행렬곱의 내부 순서가 달라지고, 그 미세한 차이가 두 logit이 거의 같을 때 순위를 뒤집을 수 있다. 둘째, GPU 커널 중 일부는 비결정적이다. 셋째, 서빙 엔진이 배치를 동적으로 구성하므로 같은 요청도 매번 다른 배치에 들어간다.

그래서 온도 0은 결정성을 보장하지 않는다. 완전한 재현이 필요하면 배치 크기를 고정하고 결정적 커널을 쓰는 등 별도 설정이 필요하며, 그 대가로 성능이 떨어진다.

이 사실은 M12의 평가 설계에 영향을 준다. 온도 0으로 평가해도 결과가 조금씩 달라지므로, 한 번의 실행으로 두 버전을 비교하면 안 된다.

온도가 하지 않는 것

확인 질문

  1. 온도가 모델의 계산 중 어느 부분을 바꾸고 어느 부분을 바꾸지 않는가?
  2. 온도 0이 완전한 결정성을 보장하지 않는 이유를 하나 말해 보라.
  3. 온도를 낮추면 환각이 줄어드는가?
확인

1. 모델의 forward와 logits는 전혀 바꾸지 않는다. logits를 확률로 변환하는 방식만 바꾼다.

2. 부동소수점 연산이 순서에 따라 미세하게 다르고, 배치 구성이 달라지면 그 순서가 바뀐다. 두 logit이 거의 같을 때 이 차이가 순위를 뒤집을 수 있다.

3. 아니다. 모델이 가장 확신하는 답을 고르게 할 뿐이고, 그 확신이 틀렸으면 확신에 찬 오답이 나온다.


Session 97top-k와 top-p

온도가 분포의 모양을 바꾼다면, top-k와 top-p는 후보를 잘라 낸다. 하나는 고정 개수로, 다른 하나는 누적 확률로 자르고, 그 차이가 상황에 따라 다른 결과를 만든다.

왜 자르는가

온도만으로는 문제가 남는다. 확률이 아주 낮은 토큰도 여전히 뽑힐 수 있다는 것이다.

어휘가 10만 개면, 각각 0.0001의 확률을 가진 토큰이 수천 개 있을 수 있다. 개별 확률은 무시할 만하지만 합치면 상당하다. 그래서 긴 생성에서 가끔 완전히 엉뚱한 토큰이 튀어나오고, 자기회귀 구조상 그 하나가 이후 전체를 망친다.

해결책은 확률이 낮은 후보를 아예 제외하는 것이다.

top-k

상위 k개만 남기고 나머지를 버린 뒤 다시 정규화한다. k=50이면 확률 순위 50위까지만 후보다.

단순하지만 문제가 있다. 분포의 모양에 관계없이 항상 같은 개수를 남긴다.

어떤 자리에서는 답이 사실상 하나로 정해져 있다. 1위가 0.98이고 나머지가 전부 미미하다. 이때 50개를 남기면 49개의 쓰레기를 후보로 유지하는 셈이다.

다른 자리에서는 수백 개의 후보가 비슷하게 그럴듯하다. 이때 50개로 자르면 좋은 후보들을 잘라 내는 것이다.

top-p

누적 확률이 p에 도달할 때까지만 남긴다. p=0.9면 확률 높은 순으로 더해 가다가 합이 0.9를 넘는 지점에서 멈춘다.

min-p

비교적 최근에 널리 쓰이게 된 방식이다. 최댓값의 일정 비율보다 낮은 후보를 버린다. minp=0.05이고 1위 확률이 0.6이면 0.03 미만인 후보를 전부 제외한다.

top-p보다 계산이 단순하고, 분포가 뾰족할 때 과도하게 좁히지 않으면서 꼬리를 잘 잘라 낸다는 평가를 받는다.

조합

실무에서는 온도와 함께 쓴다. 순서는 대개 이렇다. 온도로 분포를 조절하고, top-k나 top-p로 후보를 자르고, 남은 것에서 확률에 비례해 뽑는다.

전형적인 설정은 temperature=0.7,topp=0.9 정도이고, 형식이 중요한 작업에서는 temperature=0.2,topp=0.95 처럼 온도를 낮춘다.

한 가지 주의는 온도와 top-p를 동시에 크게 바꾸면 효과를 분리할 수 없다는 점이다. 하나씩 바꾸며 관찰하는 것이 원칙이고, 이것도 M12에서 다시 나오는 "한 번에 하나만 바꾼다"의 사례다.

확인 질문

  1. 확률이 아주 낮은 토큰들을 잘라 내야 하는 이유는?
  2. top-k의 한계를 두 가지 상황으로 설명해 보라.
  3. top-p가 기본값으로 더 널리 쓰이는 이유는?
확인

1. 개별 확률은 무시할 만해도 수천 개가 모이면 합이 상당해서, 긴 생성에서 엉뚱한 토큰이 뽑힐 수 있다. 자기회귀 구조상 그 하나가 이후 전체를 망친다.

2. 답이 사실상 하나로 정해진 자리에서는 불필요한 후보를 많이 남기고, 후보가 정말 많은 자리에서는 좋은 후보까지 잘라 낸다.

3. 분포의 모양에 따라 후보 수가 자동으로 조절되기 때문이다. 뾰족하면 좁게, 평평하면 넓게 자른다.


Session 98repetition penalty와 min-p

모델은 자기가 방금 쓴 것을 다시 쓰려는 경향이 있다. 반복 억제 장치들은 이 경향을 누르지만, 부작용도 함께 온다는 점을 알고 써야 한다.

반복이 생기는 이유

자기회귀 생성에서 어떤 문구가 나오면, 그것이 다음 예측의 조건이 된다. 그 조건 아래에서 같은 문구가 다시 나올 확률이 오르는 경우가 있다. 특히 목록이나 정형화된 구조에서 그렇다.

한 번 반복이 시작되면 강화된다. 두 번 나온 문구는 세 번째가 더 그럴듯해진다. 그래서 무한 루프에 빠지는 현상이 나타난다.

온도를 낮추면 이 문제가 심해진다. 결정적으로 고를수록 같은 상태에서 같은 선택을 하기 때문이다. 온도 0에서 반복이 가장 잘 일어난다.

억제 장치들

여기서 흔한 구현 실수가 있다. 확률이 아니라 logit에 적용해야 한다. 그리고 logit이 음수일 때 나누면 오히려 커지므로, 부호에 따라 곱하거나 나누는 처리가 필요하다. 이 세부를 틀리면 페널티가 반대로 작동한다.

부작용

여기가 중요하다. 반복 억제는 정당한 반복까지 막는다.

코드에서는 같은 변수명이 수십 번 나와야 한다. 표에서는 같은 단어가 열마다 반복된다. 인용에서는 원문을 그대로 옮겨야 한다. 목록에서는 같은 접두사가 반복된다.

repetition penalty를 강하게 걸면 이런 경우에 모델이 억지로 다른 표현을 찾아내면서 오히려 품질이 떨어진다. 코드 생성에서 변수명이 중간에 바뀌는 현상이 대표적이다.

no-repeat n-gram은 더 위험하다. 완전히 금지하므로 반드시 필요한 반복도 불가능해진다. 코드나 구조화된 출력에는 쓰지 말아야 한다.

실무 지침

확인 질문

  1. 온도를 낮추면 반복이 심해지는 이유는?
  2. repetition penalty를 확률이 아니라 logit에 적용해야 하는 이유는?
  3. 코드 생성에 반복 억제를 쓰면 안 되는 이유는?
확인

1. 결정적으로 고를수록 같은 상태에서 같은 선택을 하게 되기 때문이다. 반복 상태에 들어가면 빠져나올 무작위성이 없다.

2. logit에 적용해야 softmax를 거치며 전체가 다시 정규화되어 일관된 확률 분포가 나온다. 확률에 직접 적용하면 합이 1이 되지 않는다.

3. 코드에는 같은 변수명과 키워드가 정당하게 반복되어야 하는데, 페널티가 그것을 억제해 변수명이 중간에 바뀌는 등 오류를 만든다.


Session 99beam search는 왜 대화에 안 맞는가

beam search는 확률이 가장 높은 문장을 찾는다. 그런데 사람이 쓴 문장은 확률이 가장 높은 문장이 아니다. 이 어긋남이 열린 생성에서 beam search가 실패하는 이유다.

무엇을 하는가

greedy는 매 스텝 1위만 고른다. 그런데 지금 2위를 고르면 다음에 훨씬 좋은 선택지가 열릴 수도 있다. greedy는 그것을 볼 수 없다.

beam search는 후보 여러 개를 동시에 끌고 간다. beam이 5면 각 스텝에서 5개의 부분 문장을 유지하고, 각각을 확장한 뒤 누적 확률이 높은 5개를 다시 남긴다. 끝까지 가서 가장 높은 하나를 고른다.

번역과 요약에서는 잘 된다

정답이 대체로 하나로 정해지는 과제에서 beam search는 효과적이다. 번역에서 "The weather is nice today"의 한국어 번역은 몇 가지 표현으로 좁혀지고, 그중 확률이 가장 높은 것이 대체로 좋은 번역이다.

요약도 비슷하다. 원문의 내용이 정해져 있으므로 탐색 공간이 제한되고, 확률 최대화가 품질과 잘 정렬된다.

대화와 창작에서 실패한다

열린 생성에서는 다르다. 관측되는 증상은 이렇다.

근본 원인은 사람이 쓴 텍스트가 확률 최대가 아니라는 것이다. 사람은 예측 가능한 말만 하지 않는다. 정보를 전달하려면 어느 정도 예상 밖이어야 한다. 확률을 최대화하면 정보량이 최소화된 문장이 나온다. M1 세션 16의 언어로 말하면, 엔트로피가 0에 가까운 문장은 아무것도 말하지 않는다.

비용

또 하나의 실무적 이유가 있다. beam이 k면 매 스텝 k개의 시퀀스를 유지해야 하므로 KV cache가 k다. 세션 94의 공식에서 배치가 k배 되는 것과 같다.

메모리가 서빙 용량을 정하는 상황에서 이것은 큰 비용이다. 그래서 대부분의 LLM 서빙 엔진에서 beam search는 기본값이 아니고, 지원하더라도 권장되지 않는다.

확인 질문

  1. beam search가 번역에서는 잘 되고 대화에서는 안 되는 근본 이유는?
  2. beam search가 무미건조한 응답을 내는 이유를 확률로 설명해 보라.
  3. beam search의 메모리 비용은 어떻게 되는가?
확인

1. 번역은 정답이 대체로 하나로 좁혀져 확률 최대화가 품질과 잘 맞지만, 열린 생성에서는 사람이 쓴 텍스트가 확률 최대가 아니기 때문이다.

2. 확률이 가장 높은 문장은 가장 일반적이고 예측 가능한 문장이다. 정보를 담은 구체적인 표현은 확률이 낮으므로 탐색에서 밀려난다.

3. beam이 k개면 k개의 시퀀스를 동시에 유지해야 하므로 KV cache가 k배가 된다.


Session 100제약 디코딩과 structured output

형식을 프롬프트로 부탁하지 않는다. 문법을 상태 기계로 바꿔 허용되지 않는 토큰의 logit을 -∞로 만들면, 형식 위반이 구조적으로 불가능해진다.

프롬프트로는 왜 부족한가

"JSON으로만 답하세요"라고 쓰면 대부분 JSON이 나온다. 그런데 100번 중 두세 번은 앞에 설명을 붙이거나, 따옴표를 빠뜨리거나, 마지막 쉼표를 남긴다.

시스템이 그 응답을 파싱해야 한다면 이 실패율이 그대로 장애율이 된다. 재시도를 넣어 완화할 수 있지만 지연과 비용이 늘고, 여전히 확률적이다.

근본 문제는 프롬프트가 확률을 조정할 뿐 제약을 걸지 않는다는 것이다. 모델은 여전히 어휘 전체에서 고를 수 있다.

마스킹으로 강제하기

발상은 M4 세션 50의 causal mask와 같다. 고르면 안 되는 토큰의 logit에 -∞를 더하면 softmax 이후 확률이 정확히 0이 된다. 뽑힐 수가 없다.

그러면 "지금 시점에 허용되는 토큰"을 알아야 한다. JSON을 예로 들어 보자.

생성 시작 시점에는 {만 허용된다. { 다음에는 " 또는 }만 허용된다. 문자열 안에서는 닫는 따옴표가 나오기 전까지 대부분의 문자가 허용된다. : 다음에는 값이 시작될 수 있는 토큰만 허용된다.

현재 상태에 따라 허용 집합이 정해진다. 이것을 유한 상태 기계로 표현할 수 있고, JSON Schema 같은 명세에서 자동으로 만들 수 있다.

구현의 실제

생성 루프에 한 단계가 추가된다. logits를 얻은 뒤, 현재 상태에서 허용되는 토큰 집합을 구해 그 밖의 모든 토큰에 -∞를 더한다. 그다음 평소대로 샘플링한다. 뽑힌 토큰으로 상태를 전이시킨다.

성능 문제가 하나 있다. 매 스텝 어휘 전체에 대해 허용 여부를 판정하면 느리다. 그래서 실제 구현은 상태별 마스크를 미리 계산해 캐시한다. 이 최적화 덕분에 오버헤드가 무시할 만한 수준으로 떨어졌고, 그래서 제약 디코딩이 실용화되었다.

무엇을 보장하고 무엇을 보장하지 않는가

3중 보장

실무에서 권장되는 구성은 이렇다.

첫 번째가 대부분을 막고, 두 번째가 예상 밖의 경우를 잡고, 세 번째가 마지막 안전망이다. M11 세션 142에서 에이전트의 도구 호출에 이 구조를 그대로 적용한다.

확인 질문

  1. 프롬프트로 형식을 요구하는 것과 제약 디코딩의 차이를 한 문장으로 말해 보라.
  2. 제약 디코딩이 보장하는 것과 보장하지 않는 것은?
  3. 제약이 오히려 품질을 떨어뜨릴 수 있는 경우와 그 대응은?
확인

1. 프롬프트는 형식을 지킬 확률을 높일 뿐이고, 제약 디코딩은 형식에 맞지 않는 토큰을 아예 고를 수 없게 만들어 위반을 구조적으로 불가능하게 한다.

2. 출력이 문법과 스키마를 만족한다는 것은 보장한다. 값의 내용이 정확한지는 보장하지 않는다.

3. 모델이 추론 과정을 거쳐야 정확한데 곧바로 결론만 내라고 강제하면 정확도가 떨어진다. 스키마에 추론 필드를 앞에, 결론 필드를 뒤에 두어 생각할 여지를 준다.


Session 101speculative decoding

작은 모델이 여러 토큰을 제안하고 큰 모델이 한 번의 forward로 검증한다. 순차성을 없애지는 못하지만, 한 번의 가중치 읽기로 여러 스텝을 진행해 메모리 바운드를 우회한다. 그리고 출력 분포는 정확히 보존된다.

착상

세션 95에서 본 것을 다시 떠올리자. decode는 가중치를 읽는 데 시간을 다 쓰고 연산은 놀고 있다.

그렇다면 이런 질문이 가능하다. 어차피 가중치를 읽는 김에 토큰 하나 대신 여러 개를 처리할 수 없을까.

문제는 다음 토큰을 모르면 그다음 입력을 만들 수 없다는 것이다. 그런데 추측할 수는 있다. 작은 모델이 빠르게 5개를 추측하면, 큰 모델은 그 5개를 입력으로 받아 한 번의 forward로 각 위치의 정답 분포를 전부 계산할 수 있다. 이것은 prefill과 같은 형태라 병렬 처리가 된다.

절차

이 판정 규칙이 정교하게 설계되어 있어서, 최종 출력의 분포가 큰 모델 단독으로 생성한 것과 수학적으로 동일하다. 근사가 아니다. 이것이 speculative decoding의 결정적 장점이다. 품질을 조금도 희생하지 않는다.

얼마나 빨라지는가

k개를 제안했을 때 평균 m개가 수락된다고 하자. 한 번의 큰 모델 forward로 m+1개의 토큰이 확정되므로, 이론상 m+1배 빨라진다.

수락률은 두 모델이 얼마나 비슷한가에 달려 있다. 같은 계열의 작은 모델을 쓰면 수락률이 높다. 실무에서 2배에서 3배 정도의 속도 향상이 흔히 보고된다.

변형들

언제 효과가 큰가

반대로 배치를 크게 잡아 처리량을 최대화하는 서비스에서는 이득이 작거나 오히려 손해일 수 있다. 지연 최적화 기법이지 처리량 최적화 기법이 아니라는 점이 실무에서 중요하다.

확인 질문

  1. speculative decoding이 메모리 바운드를 우회하는 원리를 설명해 보라.
  2. 출력 품질이 떨어지지 않는다고 말할 수 있는 근거는?
  3. 배치가 클 때 효과가 줄어드는 이유는?
확인

1. 작은 모델이 여러 토큰을 제안하면 큰 모델이 그것들을 한 번의 forward로 병렬 검증할 수 있다. 가중치를 한 번 읽는 비용으로 여러 스텝을 진행하는 셈이다.

2. 수락 판정 규칙이 최종 출력 분포를 큰 모델 단독 생성과 수학적으로 동일하게 유지하도록 설계되어 있다. 근사가 아니다.

3. 배치가 크면 이미 연산이 병목에 가까워져 있어, 한 번의 읽기로 더 많은 일을 하는 이득이 작아진다.


Session 102복습 — "생성이 느립니다"

이 모듈의 실전 시험은 하나다. 응답이 느리다는 신고를 받았을 때 원인 후보를 순서대로 나열할 수 있는가.

이어 보기

프롬프트가 들어오면 토큰화, prefill, decode 루프, 디토큰화를 거친다. prefill은 프롬프트 전체를 병렬로 처리해 연산 병목이고, decode는 토큰 하나씩 순차 처리해 메모리 대역폭 병목이다. 이 비대칭이 이 모듈 전체의 뼈대다.

decode에서 이전 토큰을 매번 재계산하지 않도록 K와 V를 보관한다. Q는 현재 토큰 것만 필요하므로 저장하지 않는다. 캐시가 없으면 누적 계산이 T(T+1)/2, 있으면 T다. 대가는 메모리이고, 그 크기는 2×L×Hkv×dh×T×B×bytes다. 8B급 모델에서 토큰당 128 KiB가 기준 감각이다.

decode 스텝의 시간은 가중치를 읽는 시간이 지배한다. 그래서 양자화, GQA, 배칭, speculative decoding이 전부 읽는 양을 줄이거나 한 번의 읽기로 더 많이 처리하는 같은 목표를 향한다.

생성된 분포에서 토큰을 고르는 방식이 샘플링이다. 온도는 분포의 뾰족함을, top-k와 top-p는 후보의 범위를 조절한다. beam search는 확률 최대 문장을 찾는데, 사람의 문장은 확률 최대가 아니므로 열린 생성에는 맞지 않는다. 형식이 중요하면 부탁하지 말고 제약 디코딩으로 강제한다.

진단 순서

"응답이 느립니다"에 대한 실전 대응 순서다.

이 목록에서 M9의 항목들이 이미 여럿 나온다. 다음 모듈들에서 그것들을 채운다.

자가 점검

  1. prefill과 decode의 병목이 서로 다른 이유를 연산 강도로 설명해 보라. (S90)
  2. KV cache에서 Q를 저장하지 않는 이유는? (S92)
  3. KV cache 공식을 쓰고, 8B급 모델의 토큰당 크기를 말해 보라. (S94)
  4. decode를 빠르게 하는 기법 네 가지가 공통으로 공격하는 것은 무엇인가? (S95)
  5. 온도 0이 결정성을 보장하지 않는 이유는? (S96)
  6. top-k와 top-p의 차이, 그리고 top-p가 선호되는 이유는? (S97)
  7. 코드 생성에 repetition penalty를 쓰면 안 되는 이유는? (S98)
  8. beam search가 열린 생성에 맞지 않는 근본 이유는? (S99)
  9. 제약 디코딩이 보장하는 것과 보장하지 않는 것은? (S100)
  10. speculative decoding이 품질을 희생하지 않는다고 말할 수 있는 근거는? (S101)
확인

1. prefill은 가중치를 한 번 읽어 수천 토큰을 처리하므로 연산 강도가 높아 연산 병목이다. decode는 같은 가중치를 읽어 토큰 하나만 처리하므로 연산 강도가 1 근처라 대역폭 병목이다.

2. 새 토큰의 attention에는 자기 자신의 Q만 필요하고, 이전 토큰들의 Q는 그들의 계산이 끝났으므로 다시 쓰이지 않는다.

3. 2×L×Hkv×dh×T×B×bytes. L=32,Hkv=8,dh=128, bf16이면 토큰당 128 KiB다.

4. HBM에서 읽는 바이트 수다. 양자화와 KV 양자화는 읽을 양을 줄이고, 배칭과 speculative decoding은 한 번의 읽기로 처리하는 토큰 수를 늘린다.

5. 부동소수점 연산이 순서에 민감하고 배치 구성에 따라 그 순서가 달라지기 때문이다. 두 logit이 거의 같을 때 순위가 뒤집힐 수 있다.

6. top-k는 항상 고정 개수를, top-p는 누적 확률 기준으로 가변 개수를 남긴다. top-p는 분포의 뾰족함에 따라 후보 수가 자동 조절되어 더 널리 쓰인다.

7. 코드에는 같은 변수명과 키워드가 정당하게 반복되어야 하는데, 페널티가 그것을 억제해 변수명이 바뀌는 등의 오류를 만든다.

8. 확률이 가장 높은 문장은 가장 일반적이고 정보량이 적은 문장이기 때문이다. 사람이 쓴 텍스트는 확률 최대가 아니다.

9. 문법과 스키마를 만족한다는 것은 보장하고, 값의 내용이 정확한지는 보장하지 않는다.

10. 수락 판정 규칙이 최종 출력 분포를 큰 모델 단독 생성과 수학적으로 동일하게 유지하도록 설계되어 있기 때문이다.

모듈을 마치며

이 모듈에서 두 가지 숫자를 손에 넣었다. 토큰당 KV 128 KiBdecode는 대역폭에 묶여 있다는 사실이다. 이 둘이면 서빙 용량 산정의 뼈대가 선다.

다음 모듈은 그 아래층으로 내려간다. 왜 대역폭이 병목인지, GPU라는 하드웨어가 실제로 어떻게 생겼는지, 그리고 정밀도를 낮추면 무엇이 좋아지고 무엇을 잃는지를 열두 번의 이동에 걸쳐 본다.

M8세션 103–11412개

GPU와 메모리

왜 대역폭이 병목인지, 정밀도를 낮추면 무엇을 얻고 무엇을 잃는지. M7에서 결론만 말한 것들의 근거가 이 모듈에 있다. 관문 G3로 끝난다.

Session 103GPU가 LLM에 맞는 이유

빨라서가 아니다. 같은 연산을 대규모로 병렬 수행하고 메모리 대역폭이 압도적으로 높기 때문이다. 그리고 Transformer가 계산의 대부분을 큰 행렬곱으로 만든 것이 이 하드웨어와 맞아떨어졌다.

CPU와의 차이

CPU는 코어 수십 개에 각 코어가 복잡한 제어 회로를 갖는다. 분기 예측, 비순차 실행, 큰 캐시. 목표는 하나의 명령 흐름을 최대한 빨리 끝내는 것이다.

GPU는 반대로 설계되었다. 단순한 연산 유닛을 수만 개 두고 제어 회로는 공유한다. 목표는 같은 명령을 많은 데이터에 동시에 적용하는 것이다. 분기가 많고 데이터 의존이 복잡한 코드는 잘 못하지만, 행렬곱처럼 규칙적인 작업에서는 압도적이다.

여기에 메모리 대역폭 차이가 더해진다. 서버 CPU가 수백 GB/s인 반면 최신 GPU는 3 TB/s를 넘는다. 열 배 가까운 차이다. M7 세션 95에서 본 대로 LLM 추론은 대역폭에 묶여 있으므로 이 차이가 성능을 직접 결정한다.

구조

이 마지막 항목이 중요하다. 낮은 정밀도가 빠른 것은 읽는 양이 줄어서만이 아니라 tensor core의 처리량 자체가 다르기 때문이다. bf16이 fp32보다, fp8이 bf16보다 몇 배 빠르다.

Transformer가 맞아떨어진 이유

M0 세션 3에서 Transformer가 이긴 이유가 병렬화 가능성이라고 했다. 여기서 그 말의 하드웨어적 의미가 분명해진다.

Transformer의 계산은 대부분 큰 행렬곱이다. [T, d] × [d, d] 같은 형태이고, 이것은 tensor core가 가장 잘하는 작업이다. RNN처럼 작은 연산을 순차적으로 하는 구조는 GPU의 병렬 자원을 쓸 수 없다.

즉 Transformer의 성공은 알고리즘의 우수성만이 아니라 당시 발전하던 하드웨어와의 정렬 덕분이기도 하다. 이 관점은 새 아키텍처를 평가할 때도 유용하다. 이론적으로 효율적이어도 GPU에서 잘 돌지 않으면 실제로는 느리다.

확인 질문

  1. GPU가 CPU와 설계 목표에서 어떻게 다른가?
  2. warp 안에서 분기가 갈라지면 무슨 일이 생기는가?
  3. 낮은 정밀도가 빠른 이유 두 가지를 말해 보라.
확인

1. CPU는 하나의 명령 흐름을 최대한 빨리 끝내도록, GPU는 같은 명령을 많은 데이터에 동시에 적용하도록 설계되었다.

2. 32개 스레드가 같은 명령을 실행해야 하므로, 갈라진 양쪽을 순서대로 실행하고 해당 없는 결과를 버린다. 효율이 떨어진다.

3. 첫째, HBM에서 읽을 바이트가 줄어든다. 둘째, tensor core의 처리량 자체가 낮은 정밀도에서 몇 배 높다.


Session 104메모리 계층 — HBM과 SRAM

GPU 안에도 빠르고 작은 메모리느리고 큰 메모리가 있다. 이 계층 구조를 알아야 FlashAttention이 무엇을 했는지 이해할 수 있다.

세 층

숫자로 보면 차이가 분명하다. H100 기준 HBM 대역폭이 약 3.35 TB/s인데, SRAM은 그보다 열 배 이상 빠르다. 대신 HBM은 80GB이고 SRAM은 전체를 합쳐도 수십 MB다.

무엇이 비싼가

여기서 이 모듈 전체를 관통하는 원칙이 나온다. 연산보다 데이터 이동이 비싸다.

곱셈 하나를 하는 비용보다 그 피연산자를 HBM에서 가져오는 비용이 훨씬 크다. 그래서 GPU 코드 최적화의 상당 부분이 "계산을 줄이기"가 아니라 "HBM 왕복을 줄이기"다.

전형적인 전략은 이렇다. HBM에서 데이터 조각을 SRAM으로 가져와서, 그 조각으로 할 수 있는 모든 계산을 끝내고, 결과만 HBM에 쓴다. 조각을 잘 나누면 같은 데이터를 여러 번 읽지 않아도 된다.

연산 융합

이 원칙에서 나오는 대표적인 기법이 융합이다.

예를 들어 y=relu(x+b)를 순진하게 구현하면 두 번의 커널 실행이 된다. 덧셈 커널이 HBM에서 x를 읽어 결과를 HBM에 쓰고, ReLU 커널이 그것을 다시 읽어 결과를 HBM에 쓴다. 중간 결과가 HBM을 왕복한다.

융합하면 한 번의 커널에서 읽고, 레지스터 안에서 덧셈과 ReLU를 연달아 하고, 최종 결과만 쓴다. HBM 왕복이 절반이 된다.

이것이 torch.compile 같은 컴파일러가 하는 주요 작업 중 하나이고, 세션 113의 FlashAttention도 같은 원리를 attention에 적용한 것이다.

백엔드의 언어로

익숙한 캐시 계층 구조와 같다. 레지스터는 CPU 레지스터, SRAM은 L1/L2 캐시, HBM은 메인 메모리에 대응한다. 그리고 원칙도 같다. 가까운 곳에 데이터를 올려 두고 그것으로 최대한 많은 일을 하는 것이 빠르다.

다른 점은 GPU에서 SRAM을 프로그래머가 명시적으로 관리한다는 것이다. CPU 캐시는 하드웨어가 알아서 채우지만, GPU 공유 메모리는 어떤 데이터를 언제 올릴지 코드가 결정한다. 그래서 최적화의 여지가 크고 동시에 난이도가 높다.

확인 질문

  1. SRAM과 HBM의 차이를 속도와 용량 관점에서 말해 보라.
  2. "연산보다 데이터 이동이 비싸다"는 원칙에서 나오는 최적화 전략은?
  3. 연산 융합이 절약하는 것은 무엇인가?
확인

1. SRAM은 HBM보다 열 배 이상 빠르지만 용량이 SM당 수백 KB로 매우 작다. HBM은 수십 GB로 크지만 느리다.

2. HBM에서 데이터 조각을 SRAM으로 가져와 그것으로 할 수 있는 모든 계산을 끝낸 뒤 결과만 다시 쓰는 것이다. 같은 데이터를 여러 번 읽지 않게 한다.

3. 중간 결과의 HBM 왕복이다. 여러 연산을 하나의 커널로 합치면 중간값을 레지스터에 두고 최종 결과만 쓴다.


Session 105연산 강도와 roofline

읽은 바이트당 몇 번 연산하는가. 이 하나의 숫자가 어떤 작업이 연산에 막히는지 대역폭에 막히는지를 결정한다. roofline은 그 관계를 하나의 그래프로 보여 준다.

정의

연산 강도는 이렇게 정의한다.

I=연산횟수(FLOPs)메모리에서읽은바이트

이 값이 크면 데이터를 한 번 읽어 많은 일을 한다는 뜻이고, 작으면 읽기만 하고 거의 계산하지 않는다는 뜻이다.

임계값

GPU마다 임계값이 있다. 피크 연산 성능을 메모리 대역폭으로 나눈 값이다.

H100의 bf16 피크가 약 990 TFLOPs, 대역폭이 약 3.35 TB/s이므로,

Icrit=990×10123.35×1012295;FLOP/byte

읽은 바이트당 295번은 계산해야 GPU의 연산 능력을 다 쓸 수 있다. 그보다 적으면 대역폭에 막힌다.

roofline

이것을 그래프로 그린 것이 roofline이다. 가로축이 연산 강도, 세로축이 달성 가능한 성능이다.

왼쪽 구간은 대각선이다. 연산 강도에 비례해 성능이 오른다. 대역폭에 막혀 있어서 강도를 올리면 그만큼 이득이 있다.

오른쪽 구간은 수평선이다. 연산 능력의 상한에 도달해 더 이상 오르지 않는다.

두 구간이 만나는 지점이 임계값이다. 지붕처럼 생겨서 roofline이다.

내 작업은 지붕의 어디에 있는가

1
배치를 키우면 decode 점이 오른쪽으로 이동한다. 가중치를 한 번 읽어 더 많은 토큰을 처리하기 때문이다. 지붕의 꺾이는 지점에 도달하면 그때부터는 연산이 병목이 되어 배치를 더 키워도 처리량이 늘지 않는다.

LLM 작업들의 위치

이 배치가 M7의 모든 결론을 하나의 그림으로 설명한다. decode가 왼쪽에 있으니 대역폭을 줄이는 기법(양자화)과 강도를 올리는 기법(배칭)이 효과가 있고, prefill은 이미 오른쪽에 있으니 그런 기법의 효과가 작다.

실무에서 쓰는 법

성능 문제를 만났을 때 먼저 연산 강도를 대략 계산해 보는 것이 효율적이다.

강도가 임계값보다 훨씬 낮으면 대역폭 문제다. 읽는 양을 줄이거나(양자화, GQA) 배치를 키우는 것이 답이다. 커널을 최적화해도 소용없다.

강도가 임계값 근처거나 높으면 연산 문제다. 이때는 커널 효율, tensor core 활용, 더 낮은 정밀도가 답이다.

이 판단을 먼저 하지 않으면 효과 없는 최적화에 시간을 쓰게 된다.

확인 질문

  1. 연산 강도를 정의하고, H100의 임계값이 왜 300 근처인지 계산해 보라.
  2. decode 배치 1의 연산 강도가 1 근처인 이유는?
  3. 연산 강도가 임계값보다 훨씬 낮을 때 효과가 있는 최적화와 없는 최적화를 구분해 보라.
확인

1. 읽은 바이트당 수행하는 FLOPs다. 임계값은 피크 연산 성능을 메모리 대역폭으로 나눈 값이고, H100 bf16이면 990e12/3.35e12295다.

2. 모델 가중치 전체를 읽어 토큰 하나분의 연산만 하기 때문이다. 파라미터당 2 FLOPs를 하고 파라미터당 2바이트를 읽으므로 대략 1이 된다.

3. 효과가 있는 것은 읽는 바이트를 줄이는 양자화·GQA와 강도를 올리는 배칭이다. 효과가 없는 것은 커널 최적화나 연산량 감소다.

앉아서 읽을 것

Horace He의 Making Deep Learning Go Brrrr From First Principles가 이 주제에 대한 가장 좋은 글이다. 연산 바운드, 대역폭 바운드, 오버헤드 바운드의 세 구분이 특히 유용하다.


Session 106VRAM은 어디에 쓰이는가

네 덩어리로 나뉜다. 파라미터, 기울기와 옵티마이저 상태, 활성값, KV cache. 학습이냐 추론이냐에 따라 어느 것이 있고 없는지가 달라지고, 그것이 필요 GPU를 결정한다.

네 덩어리

학습과 추론은 메모리 구성이 거의 정반대다. 학습은 옵티마이저와 활성값이 지배하고, 추론은 파라미터와 KV cache가 지배한다.

학습 쪽 계산

8B 모델을 bf16으로 학습한다고 하자. 다만 옵티마이저 상태는 정밀도 문제로 fp32로 유지하는 것이 일반적이다.

항목계산크기
파라미터 (bf16)8e9×216 GB
기울기 (bf16)8e9×216 GB
옵티마이저 m, v (fp32)8e9×4×264 GB
활성값배치·길이에 따라수십 GB
합계100 GB 이상

80GB 카드 한 장으로는 불가능하다는 결론이 여기서 나온다. 그래서 M6의 LoRA가 필요하고, 그것도 부족하면 여러 GPU에 나눠야 한다.

추론 쪽 계산

같은 모델을 추론한다면 이렇다.

항목계산크기
파라미터 (bf16)8e9×216 GB
활성값작음1~2 GB
KV cache컨텍스트 × 동시성 × 128 KiB가변

파라미터가 고정 비용이고 KV cache가 변동 비용이다. 그리고 이 변동 비용이 서비스 용량을 정한다.

활성값 체크포인팅

학습에서 활성값이 문제가 될 때 쓰는 기법이 있다. 모든 중간 결과를 저장하는 대신 일부만 저장하고, 역전파에서 필요하면 다시 계산한다.

M2 세션 24에서 언급한 activation checkpointing이다. 연산이 대략 30퍼센트 늘어나는 대신 활성값 메모리가 크게 줄어든다. 메모리와 연산을 맞바꾸는 전형적인 거래이고, 큰 모델 학습에서는 사실상 필수다.

확인 질문

  1. VRAM의 네 덩어리를 말하고, 학습과 추론에서 각각 어느 것이 있는지 구분해 보라.
  2. 8B 모델의 전체 파인튜닝에 100GB가 넘게 필요한 이유를 항목별로 설명해 보라.
  3. activation checkpointing이 맞바꾸는 두 가지는?
확인

1. 파라미터, 기울기와 옵티마이저 상태, 활성값, KV cache다. 학습에는 앞 세 개가 있고 KV cache가 없다. 추론에는 파라미터와 작은 활성값, 그리고 KV cache가 있다.

2. 파라미터 16GB, 기울기 16GB, AdamW의 1차·2차 모멘트가 fp32로 64GB, 여기에 활성값이 더해진다.

3. 메모리와 연산이다. 활성값을 버리고 역전파에서 다시 계산하므로 연산이 약 30퍼센트 늘고 메모리가 크게 준다.


Session 107모델 크기에서 VRAM 추산하기

설정 파일 여섯 개 값과 목표 컨텍스트·동시성만 있으면 필요한 GPU를 손으로 계산할 수 있다. 이 계산이 관문 G3의 내용이다.

필요한 값

모델 설정에서 다음을 읽는다. hidden_size(d), num_hidden_layers(L), intermediate_size(d_ff), vocab_size(V), num_attention_heads, num_key_value_heads(H_kv).

여기서 헤드 차원은 d / num_attention_heads다.

절차

PL×big(4d2;또는GQA보정+3d,dffbig)+2Vd
pertoken=2×L×Hkv×dh×bytes

예제

8B 모델(L=32,d=4096,Hkv=8,dh=128)을 80GB GPU에서 bf16으로 서비스한다고 하자.

가중치는 16GB다. 여유 8GB를 빼면 KV 예산이 56GB다.

토큰당 KV는 2×32×8×128×2=128KiB다.

56×1024,MiB0.125,MiB458,000;토큰

컨텍스트를 8k로 잡으면 동시 요청 약 56개, 32k로 잡으면 약 14개다.

조정 수단

계산 결과가 요구에 못 미치면 선택지는 이렇다.

확인 질문

  1. VRAM 산정의 다섯 단계를 순서대로 말해 보라.
  2. 80GB GPU, 8B bf16 모델, 컨텍스트 16k일 때 동시 요청 수를 계산해 보라.
  3. KV cache 양자화가 가중치 양자화보다 동시성에 더 효과적인 이유는?
확인

1. 파라미터 수 계산 → 가중치 메모리 → 토큰당 KV 크기 → KV 예산(총 VRAM − 가중치 − 여유) → 예산을 컨텍스트로 나눠 동시성.

2. 예산 56GB를 토큰당 128 KiB로 나누면 약 458,000토큰, 이를 16384로 나누면 약 28개다.

3. 가중치는 고정 비용이라 줄여도 예산이 조금 늘 뿐이지만, KV는 요청마다 드는 변동 비용이라 절반이 되면 같은 예산으로 두 배의 요청을 받을 수 있다.


Session 108부동소수점 포맷

FP32, FP16, BF16, FP8은 같은 비트 수를 지수와 가수에 다르게 나눈 결과다. 지수가 범위를, 가수가 정밀도를 결정하고, 이 배분의 차이가 학습 안정성을 가른다.

구조

부동소수점 수는 세 부분으로 이루어진다. 부호 1비트, 지수부, 가수부다.

=(1)s×2ebias×1.m

네 가지 배분

포맷전체지수가수성격
FP3232823기준. 범위도 정밀도도 충분
FP1616510정밀도는 괜찮지만 범위가 좁다
BF161687범위는 FP32와 같고 정밀도를 희생
FP8 (E4M3)843추론 위주. 범위와 정밀도 모두 빠듯

같은 16비트, 다른 배분

BF16은 FP16과 같은 16비트인데 지수를 FP32와 똑같이 8비트로 두고 가수를 깎았다. 그래서 표현 범위가 FP32와 같아 오버플로·언더플로가 나지 않고, 대신 값의 해상도가 거칠다. 학습에서는 이 선택이 압도적으로 유리했다.

무엇이 문제였나

FP16은 지수가 5비트라 표현 범위가 좁다. 대략 6e-5에서 65504 사이다.

학습에서 기울기는 아주 작은 값이 되기 쉽다. M1 세션 20에서 본 기울기 소실이 극단적이지 않더라도, 층이 깊으면 기울기가 1e-6 수준으로 내려간다. 그러면 FP16에서 언더플로되어 0이 된다. 학습 신호가 사라진다.

반대로 어떤 값이 65504를 넘으면 inf가 되고, M1 세션 15에서 본 대로 NaN으로 이어진다.

이 문제를 우회하려고 손실 스케일링이라는 기법이 쓰였다. 손실에 큰 수를 곱해 기울기를 표현 범위 안으로 끌어올리고, 갱신 직전에 다시 나눈다. 동작은 하지만 스케일을 동적으로 조정해야 하고, 잘못되면 학습이 깨진다.

BF16의 해법

BF16은 지수를 8비트로 유지해 FP32와 정확히 같은 범위를 갖는다. 언더플로도 오버플로도 사실상 일어나지 않는다. 손실 스케일링이 필요 없다.

대가는 가수 7비트다. 인접한 값 사이가 성기다. 그런데 신경망 학습에서는 이것이 큰 문제가 되지 않았다. 기울기 자체가 노이즈가 많은 추정치이고, 확률적 경사하강은 어느 정도의 부정확성을 견디도록 설계되어 있기 때문이다.

확인 질문

  1. 지수부와 가수부가 각각 무엇을 결정하는가?
  2. FP16이 학습에서 언더플로를 일으키는 상황을 설명해 보라.
  3. BF16이 FP16과 같은 비트 수로 다른 성질을 갖는 이유는?
확인

1. 지수부는 표현 가능한 값의 범위를, 가수부는 인접한 값 사이의 촘촘함, 즉 정밀도를 결정한다.

2. 깊은 신경망에서 기울기가 1e-6 수준으로 작아지면 FP16의 최소 표현값 아래로 내려가 0이 된다. 학습 신호가 사라진다.

3. 같은 16비트를 지수와 가수에 다르게 나눴기 때문이다. BF16은 지수를 8비트로 FP32와 같게 유지하고 가수를 7비트로 줄였다.


Session 109왜 bf16이 학습의 기본이 되었나

범위가 정밀도를 이긴다. 신경망 학습은 값의 정확한 크기보다 값이 표현 범위 안에 있는지가 훨씬 중요하고, 이 사실이 BF16을 표준으로 만들었다.

학습이 요구하는 것

학습에서 다루는 값들의 범위를 생각해 보자.

활성값은 정규화 덕분에 대체로 1 근처다. 가중치도 초기화와 weight decay로 통제된다. 문제는 기울기다. 층에 따라, 학습 단계에 따라 크기가 여러 자릿수를 오간다. 초기에는 크고, 수렴하면서 작아지고, 어떤 층은 다른 층보다 훨씬 작다.

기울기는 넓은 동적 범위를 요구한다. 정밀도는 상대적으로 덜 중요하다. 기울기는 미니배치에서 추정한 노이즈 섞인 값이고, 소수점 아래 몇 자리가 부정확해도 평균적으로 옳은 방향이면 학습이 진행된다.

BF16은 정확히 이 요구에 맞다. 넓은 범위, 거친 정밀도.

혼합 정밀도

실제 학습은 순수 BF16이 아니라 혼합 정밀도로 한다.

FP32 마스터 사본을 두면 갱신이 정확히 누적되고, 계산할 때만 BF16으로 변환해 쓴다. 이것이 M8 세션 106의 메모리 표에서 옵티마이저 상태를 fp32로 계산한 이유다.

추론에서는 다르다

추론에는 누적이 없다. 한 번 계산하고 끝이다. 그래서 정밀도 요구가 학습보다 훨씬 낮고, 더 공격적으로 줄일 수 있다.

이것이 다음 세션부터의 양자화 이야기다. 추론 전용으로 INT8이나 INT4까지 내려가는 것이 실용적인 이유가 여기 있다.

FP8과 그 이후

최근 하드웨어는 FP8을 지원한다. 학습에도 쓰이기 시작했는데, 범위가 좁아 스케일링을 세심하게 관리해야 한다. 층별로, 텐서별로 스케일을 따로 두는 식이다.

일반적인 흐름은 명확하다. 하드웨어가 더 낮은 정밀도를 빠르게 처리할수록 그쪽으로 이동하고, 그때마다 범위 관리 기법이 함께 발전한다.

확인 질문

  1. 학습에서 정밀도보다 범위가 중요한 이유를 기울기의 성질로 설명해 보라.
  2. 파라미터의 FP32 마스터 사본을 유지하는 이유는?
  3. 추론이 학습보다 낮은 정밀도를 견디는 이유는?
확인

1. 기울기는 층과 학습 단계에 따라 여러 자릿수를 오가므로 넓은 동적 범위가 필요하다. 반면 기울기 자체가 노이즈 섞인 추정치라 소수점 아래 정밀도는 덜 중요하다.

2. 학습률이 작으면 한 스텝의 갱신량이 파라미터에 비해 아주 작은데, 낮은 정밀도에서는 그 변화가 반올림으로 사라진다. FP32 사본에 누적해야 미세한 변화가 쌓인다.

3. 추론에는 갱신의 누적이 없기 때문이다. 한 번 계산하고 끝이라 반올림 오차가 쌓이지 않는다.


Session 110양자화의 원리

실수 범위를 정수 격자에 대응시킨다. 필요한 것은 스케일제로포인트 두 값뿐이고, 이 단순한 변환이 메모리와 대역폭을 몇 배로 줄인다.

기본 변환

가중치들이 대략 -0.8에서 0.9 사이에 분포한다고 하자. 이것을 INT8, 즉 -128에서 127 사이의 정수로 바꾸고 싶다.

먼저 스케일을 정한다. 실수 범위의 폭을 정수 범위의 폭으로 나눈 값이다.

s=maxmin2b1

그다음 각 값을 이렇게 바꾼다.

q=round!left(xsright)+z

z가 제로포인트다. 실수 0이 정수 몇 번에 대응하는지를 나타낸다.

되돌릴 때는 반대로 한다.

hatx=s×(qz)

round 때문에 x이 정확히 같지는 않다. 이 차이가 양자화 오차다.

대칭과 비대칭

활성값은 ReLU 이후처럼 한쪽으로 치우친 경우가 많아 비대칭이 유리할 때가 있다.

무엇을 단위로 하는가

스케일을 어느 범위마다 하나씩 둘 것인가도 선택이다.

무엇이 줄어드는가

INT8이면 bf16 대비 절반, INT4면 4분의 1이다. 그리고 M7 세션 95에서 본 대로 decode의 시간은 읽는 바이트가 지배하므로, 이 감소가 거의 그대로 속도 향상으로 이어진다.

메모리도 그만큼 줄어 M8 세션 107의 예산 계산이 여유로워진다. 8B 모델을 INT4로 양자화하면 가중치가 4GB가 되어, 24GB 카드에서도 상당한 KV 예산을 확보할 수 있다.

확인 질문

  1. 양자화에 필요한 두 값은 무엇이며 각각 무슨 역할을 하는가?
  2. per-tensor 양자화가 극단값에 취약한 이유는?
  3. INT4 양자화에서 per-group이 필요한 이유는?
확인

1. 스케일과 제로포인트다. 스케일은 실수 범위를 정수 격자에 대응시키는 비율이고, 제로포인트는 실수 0이 어느 정수에 대응하는지를 정한다.

2. 텐서 전체에 스케일 하나를 쓰므로, 극단적으로 큰 값 하나가 스케일을 키운다. 그러면 나머지 대부분의 값이 몇 개의 정수로 뭉개져 해상도를 잃는다.

3. INT4는 표현할 수 있는 값이 16개뿐이라 해상도가 극도로 낮다. 작은 그룹마다 스케일을 따로 두어야 각 그룹의 분포에 맞는 해상도를 확보할 수 있다.


Session 111양자화 방식들

PTQ와 QAT, weight-only와 weight+activation의 구분이 먼저다. 그다음 GPTQ, AWQ, NF4, GGUF가 각각 무엇을 다르게 하는지가 온다.

두 개의 축

이 두 번째 구분이 실무에서 중요하다. decode에서는 weight-only로 충분하다. 시간을 쓰는 것이 가중치 읽기이기 때문이다. 활성값은 배치 1이면 크기가 작아 읽기 비용에 거의 기여하지 않는다.

반대로 prefill이나 학습처럼 연산이 병목인 경우에는 활성값까지 낮춰야 tensor core의 저정밀 처리량 이득을 얻는다.

주요 방식들

무엇을 고르는가

확인 질문

  1. PTQ와 QAT의 차이와, 실무 기본값은?
  2. decode에 weight-only 양자화로 충분한 이유는?
  3. AWQ의 핵심 관찰은 무엇인가?
확인

1. PTQ는 학습 후에 양자화하고 QAT는 학습 과정에 포함시킨다. PTQ가 싸고 빨라 실무의 기본이다.

2. decode의 시간은 가중치를 읽는 데 쓰이고 활성값은 배치가 작아 읽기 비용에 거의 기여하지 않기 때문이다.

3. 모든 가중치가 똑같이 중요하지 않고, 활성값이 큰 채널에 대응하는 가중치가 특히 중요하다는 것이다. 그 채널을 보호하면 같은 비트에서 품질이 좋아진다.


Session 112양자화의 대가

"품질이 조금 떨어진다"는 말로는 부족하다. 무엇이 얼마나 떨어지는지 측정해야 하고, 특히 outlier 채널이라는 구조적 문제를 알아야 한다.

outlier 문제

큰 모델의 활성값을 관찰하면 이상한 현상이 보인다. 대부분의 값이 작은데 특정 채널에서만 수십 배 큰 값이 나온다. 그리고 그 채널은 층마다 고정되어 있다.

이 outlier가 양자화를 망친다. per-tensor 스케일을 쓰면 그 큰 값 때문에 스케일이 커지고, 나머지 99퍼센트의 값이 몇 개의 정수로 뭉개진다. 정보가 대량으로 손실된다.

그런데 이 outlier 채널이 성능에 중요하다는 것이 밝혀졌다. 그냥 잘라 내면 모델이 크게 망가진다.

대응은 둘이다. outlier 채널만 높은 정밀도로 유지하고 나머지를 양자화하는 혼합 방식, 또는 앞 세션의 AWQ처럼 중요한 채널을 보호하도록 스케일을 재배치하는 방식이다.

이 현상은 모델이 클수록 심해진다. 그래서 작은 모델에서 잘 되던 양자화가 큰 모델에서 무너지는 경우가 있고, 반드시 대상 모델에서 직접 측정해야 하는 이유다.

무엇이 먼저 무너지는가

양자화의 영향은 과제마다 다르다. 관측되는 경향은 이렇다.

평균적인 벤치마크 점수는 거의 안 떨어지는데 특정 과제에서 크게 떨어질 수 있다. 이것이 "측정해야 한다"고 말하는 이유다.

어떻게 측정하는가

세 층으로 본다.

그리고 비교는 반드시 같은 조건에서 한다. 같은 프롬프트, 같은 샘플링 설정, 같은 시드로 원본과 양자화 모델을 나란히 돌린다.

판단

정리하면 양자화는 이런 거래다.

확인 질문

  1. outlier 채널이 무엇이며 왜 양자화를 어렵게 하는가?
  2. 양자화 후 가장 먼저 무너지는 능력의 종류를 두 가지 말해 보라.
  3. 양자화 품질을 측정할 때 perplexity만으로 부족한 이유는?
확인

1. 특정 채널에서만 수십 배 큰 활성값이 나오는 현상이다. per-tensor 스케일이 그 값에 맞춰 커지면 나머지 값들이 몇 개의 정수로 뭉개져 정보가 손실된다.

2. 여러 단계를 거치는 긴 추론(오차 누적)과 드문 지식(소수의 가중치에 의존)이다.

3. perplexity는 민감도가 낮아 평균적인 언어 능력만 보여 준다. 특정 과제에서의 큰 열화를 놓칠 수 있어 실제 과제 평가와 자체 평가셋이 필요하다.


Session 113FlashAttention

근사가 아니다. 수학적으로 완전히 같은 결과를 내면서 [T, T] 점수 행렬을 HBM에 만들지 않는다. tiling과 online softmax가 그것을 가능하게 한다.

문제

M4 세션 54에서 본 대로 attention의 메모리 병목은 점수 행렬이다. [B, H, T, T]를 HBM에 쓰고, softmax를 위해 다시 읽고, 결과를 쓰고, V와 곱하려고 또 읽는다.

세션 104의 원칙으로 보면 이것은 최악이다. 거대한 중간 결과가 HBM을 여러 번 왕복한다. 계산량 자체는 그대로인데 데이터 이동이 지배한다.

발상

조각으로 나눠 SRAM 안에서 처리하면 된다. Q를 블록으로, K와 V도 블록으로 나눈다. Q 블록 하나를 SRAM에 올려 두고, K·V 블록을 차례로 가져와 부분 결과를 누적한다.

문제는 softmax다. softmax는 전체 합으로 나눠야 하는데, 조각으로 계산하면 전체 합을 아직 모른다. 앞부분을 계산할 때 뒷부분에 어떤 값이 나올지 알 수 없다.

online softmax

해결책이 M1 세션 15의 log-sum-exp 트릭의 확장이다.

지금까지 본 최댓값 m과 지수합 l을 들고 다닌다. 새 블록을 처리할 때 그 블록의 최댓값이 기존보다 크면, 지금까지 누적한 결과를 보정한다.

mnew=max(mold,mblock)
lnew=emoldmnewlold+emblockmnewlblock

누적된 출력에도 같은 보정 계수를 곱한다. 이렇게 하면 마지막 블록까지 처리했을 때 결과가 전체를 한 번에 계산한 것과 정확히 같다.

이것이 "근사가 아니다"라는 말의 근거다. 계산 순서를 바꿨을 뿐 수학적으로 동등하다.

무엇이 좋아지는가

세대별 변화

이 흐름에서 보이는 것은 커널 최적화가 하드웨어 세대마다 다시 설계되어야 한다는 사실이다.

PagedAttention과의 관계

이름이 비슷해 혼동하기 쉬운데 다른 층위의 기술이다.

둘은 경쟁 관계가 아니라 직교한다. 실제 서빙 엔진은 둘을 함께 쓴다.

확인 질문

  1. FlashAttention이 "근사가 아니다"라고 말할 수 있는 근거는?
  2. online softmax가 해결하는 문제와 그 방법을 설명해 보라.
  3. FlashAttention과 PagedAttention은 각각 어느 계층의 문제를 푸는가?
확인

1. 계산 순서를 바꿨을 뿐 수학적으로 동등한 결과를 내기 때문이다. 최댓값과 지수합을 들고 다니며 보정하므로 마지막에는 전체를 한 번에 계산한 것과 같은 값이 나온다.

2. 조각으로 나눠 계산하면 softmax의 전체 합을 모른다는 문제다. 지금까지의 최댓값과 지수합을 유지하다가 새 블록의 최댓값이 크면 누적된 결과에 보정 계수를 곱해 갱신한다.

3. FlashAttention은 커널 계층에서 HBM 왕복을 줄이고, PagedAttention은 메모리 관리 계층에서 KV cache의 단편화를 줄인다. 서로 직교해 함께 쓴다.

앉아서 읽을 것

FlashAttention 논문의 3절에 tiling과 online softmax의 수식이 있다. 이 세션을 읽은 뒤라면 재귀식을 따라갈 수 있다. 최신 세대가 궁금하면 FlashAttention-4를 보되, 원리는 FA1에 다 있다.


Session 114복습 — VRAM을 소리 내어 계산하기

관문 G3다. 모델·컨텍스트·동시성이 주어졌을 때 필요한 VRAM과 가능한 동시성을 계산기 없이 논리로 답할 수 있어야 한다.

이어 보기

GPU는 같은 연산을 대규모 병렬로 수행하고 메모리 대역폭이 높아서 LLM에 맞다. 그 안에도 계층이 있어서 SRAM은 빠르고 작고 HBM은 크고 느리다. 연산보다 데이터 이동이 비싸다는 원칙이 여기서 나온다.

어떤 작업이 무엇에 막히는지는 연산 강도로 판단한다. 읽은 바이트당 연산 횟수이고, H100에서 임계값이 약 300이다. decode 배치 1은 강도가 1 근처라 완전히 대역폭에 막혀 있고, prefill은 수천이라 연산에 막혀 있다. 배치를 키우면 decode 점이 오른쪽으로 이동한다.

VRAM은 네 덩어리다. 학습은 옵티마이저와 활성값이 지배하고, 추론은 파라미터와 KV cache가 지배한다. 추론에서 파라미터는 고정 비용, KV cache는 컨텍스트 × 동시성의 변동 비용이다.

정밀도는 지수와 가수의 배분 문제다. BF16이 표준이 된 이유는 지수를 FP32와 같게 유지해 범위가 정밀도를 이겼기 때문이다. 추론에는 누적이 없어 더 낮출 수 있고, 그것이 양자화다. 스케일과 제로포인트로 정수 격자에 대응시키며, outlier 채널이 주된 난점이다.

attention의 [T, T] 행렬은 FlashAttention이 HBM에 만들지 않는 방식으로 처리한다. online softmax 덕분에 근사가 아니라 정확한 계산이다.

관문 G3

  1. 연산 강도를 정의하고, decode 배치 1과 prefill의 값을 각각 대라. (S105)
  2. VRAM의 네 덩어리를 말하고, 학습과 추론에서 각각 무엇이 지배하는지 말해 보라. (S106)
  3. 8B 모델(L=32,Hkv=8,dh=128, bf16)의 토큰당 KV 크기를 계산해 보라. (S107)
  4. 80GB GPU에 위 모델을 올리고 컨텍스트 32k를 지원할 때 동시 요청 수는? (S107)
  5. 같은 상황에서 동시성을 두 배로 늘리려면 무엇을 하겠는가? 두 가지 이상. (S107)
  6. BF16이 FP16을 대체한 이유를 지수·가수 배분으로 설명해 보라. (S108, S109)
  7. 학습에서 파라미터의 FP32 마스터 사본이 필요한 이유는? (S109)
  8. decode에 weight-only 양자화로 충분한 이유는? (S111)
  9. outlier 채널 문제와 그 대응 두 가지를 말해 보라. (S112)
  10. FlashAttention이 정확한 계산인 이유와, PagedAttention과의 계층 차이를 말해 보라. (S113)
확인

1. 읽은 바이트당 수행하는 FLOPs다. decode 배치 1은 약 1, prefill은 프롬프트 길이만큼 커서 수천에 이른다. H100 임계값은 약 295다.

2. 파라미터, 기울기·옵티마이저 상태, 활성값, KV cache다. 학습은 옵티마이저 상태와 활성값이, 추론은 파라미터와 KV cache가 지배한다.

3. 2×32×8×128×2=131072 바이트 = 128 KiB다.

4. 가중치 16GB와 여유 8GB를 빼면 56GB. 요청 하나가 32768×128KiB=4GiB를 쓰므로 약 14개다.

5. KV cache를 8비트로 양자화하면 토큰당 64KiB가 되어 동시성이 두 배가 된다. 가중치를 INT4로 양자화하면 예산이 12GB 늘어 약 3개가 추가된다. 컨텍스트 상한을 16k로 낮추면 두 배가 된다. GQA 헤드가 더 적은 모델로 바꾸는 것도 방법이다.

6. 같은 16비트를 다르게 나눴다. FP16은 지수 5비트라 범위가 좁아 작은 기울기가 언더플로된다. BF16은 지수 8비트로 FP32와 같은 범위를 갖고 가수를 7비트로 줄였다. 학습에서는 범위가 정밀도보다 중요하다.

7. 학습률이 작으면 한 스텝의 갱신량이 파라미터에 비해 너무 작아 저정밀도에서 반올림으로 사라진다. FP32 사본에 누적해야 미세한 변화가 쌓인다.

8. decode의 시간은 가중치를 HBM에서 읽는 데 쓰이고, 활성값은 배치가 작아 읽기 비용에 거의 기여하지 않기 때문이다.

9. 특정 채널에서만 수십 배 큰 활성값이 나와 per-tensor 스케일을 망친다. 대응은 그 채널만 높은 정밀도로 유지하는 혼합 방식과, AWQ처럼 중요한 채널을 보호하도록 스케일을 재배치하는 방식이다.

10. online softmax로 최댓값과 지수합을 들고 다니며 보정하므로 전체를 한 번에 계산한 것과 수학적으로 동일하다. FlashAttention은 커널 계층, PagedAttention은 메모리 관리 계층의 기술이고 서로 직교한다.

모듈을 마치며

이제 왜 느린지에 대한 답이 다 나왔다. decode는 대역폭에 막혀 있고, 그 대역폭을 결정하는 것은 읽어야 할 바이트 수이며, 그것을 줄이는 수단이 양자화와 GQA이고, 한 번의 읽기로 더 많이 처리하는 수단이 배칭이다.

다음 모듈은 엔진이 그것을 어떻게 푸는가다. 배칭을 실제로 어떻게 구현하는지, KV cache의 메모리를 어떻게 관리하는지, 그리고 그 결과 처리량과 지연이 어떻게 움직이는지를 열두 번의 이동에 걸쳐 본다. 여기가 백엔드 경험이 가장 직접적으로 쓰이는 모듈이다.

M9세션 115–12612개

서빙 시스템

엔진이 M7·M8의 제약을 어떻게 푸는가. 요청 스케줄링, 메모리 관리, 용량 산정 — 백엔드 경험이 가장 직접적으로 쓰이는 모듈이다.

Session 115배칭의 진화

정적 배칭에서는 가장 긴 요청이 끝날 때까지 나머지 슬롯이 논다. 이 낭비를 없애는 과정이 배칭 기법의 역사이고, 그 종착점이 다음 세션의 continuous batching이다.

왜 배치가 필요한가

M8 세션 105에서 확정한 대로, decode는 연산 강도가 1 근처라 대역폭에 완전히 막혀 있다. 가중치 16GB를 읽어 토큰 하나를 만든다.

그런데 같은 16GB를 읽어 요청 서른 개의 토큰을 동시에 만들 수 있다. 읽기 비용이 서른 개에 분산 상환되고, 연산 강도가 30 근처로 오른다. 처리량이 거의 서른 배가 되는데 스텝 시간은 거의 그대로다.

이것이 배칭이 LLM 서빙에서 선택이 아니라 필수인 이유다.

정적 배칭

가장 단순한 방식이다. 요청이 N개 모일 때까지 기다렸다가 한꺼번에 처리하고, 전부 끝나면 다음 배치를 시작한다.

문제는 응답 길이가 제각각이라는 점이다. 배치에 20토큰짜리 요청과 800토큰짜리 요청이 섞여 있으면, 20토큰 요청은 780스텝 동안 아무것도 하지 않는 슬롯을 차지한다.

이 낭비가 얼마나 큰지는 응답 길이 분포에 달려 있다. 실제 서비스의 길이 분포는 대개 꼬리가 길어서, 정적 배칭의 GPU 가동률이 30에서 50퍼센트에 머무는 경우가 흔했다.

두 번째 문제도 있다. 배치가 다 끝날 때까지 새 요청이 기다린다. 방금 도착한 짧은 요청이 앞선 배치의 긴 요청 때문에 수백 스텝을 대기한다.

동적 배칭

첫 개선은 배치 구성을 유연하게 하는 것이다. 고정된 N을 기다리지 않고, 짧은 시간 창 안에 도착한 요청들을 모아 배치를 만든다.

대기 시간이 줄어들지만 핵심 문제는 남는다. 배치가 시작되면 그 배치는 끝까지 함께 간다. 슬롯이 노는 문제가 그대로다.

근본 원인

관찰해 보면 문제의 뿌리는 배치의 단위가 요청이라는 것이다. 요청 단위로 묶으면 요청이 끝날 때까지 슬롯이 묶인다.

그런데 decode는 스텝 단위로 진행된다. 매 스텝마다 배치를 새로 구성할 수 있다면, 끝난 요청을 빼고 새 요청을 넣을 수 있다.

이 발상 전환이 continuous batching이고 다음 세션의 주제다. 백엔드의 언어로 말하면 작업 단위를 요청에서 스텝으로 낮춘 것이다.

확인 질문

  1. decode에서 배칭이 처리량을 크게 올리는 이유를 연산 강도로 설명해 보라.
  2. 정적 배칭에서 GPU가 노는 두 가지 상황을 말해 보라.
  3. 정적·동적 배칭이 공유하는 근본 한계는 무엇인가?
확인

1. 가중치를 한 번 읽어 여러 요청의 토큰을 동시에 만들면 읽기 비용이 분산 상환되어 연산 강도가 배치 크기만큼 올라간다. 스텝 시간은 거의 그대로인데 처리량이 배가된다.

2. 짧은 응답이 먼저 끝나도 배치가 끝날 때까지 그 슬롯이 비어 있고, 새로 도착한 요청이 현재 배치가 끝날 때까지 대기한다.

3. 배치의 단위가 요청이라는 점이다. 요청이 끝날 때까지 슬롯이 묶여 있어 중간에 교체할 수 없다.


Session 116continuous batching

배치를 디코딩 스텝 단위로 재구성한다. 끝난 요청을 빼고 대기 중인 요청을 즉시 넣는다. 같은 GPU에서 처리량이 2~4배가 되는 이유가 이 한 가지 변경에 있다.

무엇을 바꾸는가

스케줄러가 매 스텝마다 실행 중인 요청 집합을 다시 정한다.

이렇게 하면 슬롯이 비어 있는 시간이 사라진다. 정확히는 다음 요청이 큐에 있는 한 사라진다.

같은 요청, 다른 스케줄

가로가 디코딩 스텝, 세로가 GPU 슬롯이다. 빗금이 노는 슬롯, 주황이 나중에 들어온 요청이다. 두 방식의 차이는 오직 "언제 다음 요청을 넣는가" 하나인데, 완료 시점과 가동률이 함께 개선된다.

새로 들어온 요청의 prefill

한 가지 세부가 중요하다. 새 요청이 배치에 합류하려면 먼저 prefill을 해야 한다. 그런데 이 prefill은 기존 요청들의 decode와 성격이 다르다. 토큰이 수천 개다.

순진하게 구현하면 그 스텝이 갑자기 길어진다. 기존 요청들의 토큰 하나를 만드는 데 20ms면 되는데, 새 요청의 prefill 2000토큰 때문에 그 스텝이 200ms가 되는 식이다.

스케줄러의 결정

매 스텝 스케줄러가 판단해야 하는 것들이 있다.

백엔드의 언어로

이것은 정확히 요청 스케줄링 문제다. 익숙한 개념이 그대로 대응된다.

배치가 워커 풀이고, 스텝이 타임 슬라이스이며, 스케줄러가 매 슬라이스마다 실행 집합을 정한다. preemption은 우선순위 기반 선점이고, 큐 정책은 FCFS와 SJF의 트레이드오프다.

확인 질문

  1. continuous batching이 정적 배칭과 다른 점을 한 문장으로 말해 보라.
  2. 새 요청의 prefill이 기존 요청들에 영향을 주는 이유는?
  3. 스케줄러가 메모리 부족에 대응하는 방법은?
확인

1. 배치를 요청 단위가 아니라 디코딩 스텝 단위로 재구성해, 끝난 요청을 빼고 대기 요청을 즉시 넣는다.

2. prefill은 수천 토큰을 한 번에 처리하므로 그 스텝이 길어진다. 같은 배치에 있던 기존 요청들의 토큰 생성이 그만큼 지연되어 사용자가 멈춤을 경험한다.

3. 실행 중인 요청 일부를 중단하고 그 KV cache를 회수한다(preemption). 중단된 요청은 나중에 재개하며, KV를 재계산하거나 CPU 메모리에 옮겨 뒀다 되가져온다.

앉아서 읽을 것

vLLM 블로그의 Inside vLLM: Anatomy of a High-Throughput LLM Inference System이 실제 엔진의 스케줄러 구조를 자세히 설명한다. 원조 논문은 Orca (OSDI'22)다.


Session 117PagedAttention

KV cache를 연속 할당하지 않고 고정 크기 블록으로 쪼개 관리한다. 운영체제의 가상 메모리 페이징을 그대로 가져온 것이고, 같은 GPU에서 동시 요청 수를 2~4배 늘린다.

문제

continuous batching을 구현하려 하면 메모리 관리에서 막힌다.

요청이 들어올 때 그 요청이 최종적으로 몇 토큰을 쓸지 알 수 없다. 20토큰에서 끝날 수도, 4000토큰까지 갈 수도 있다.

순진한 구현은 최대 길이만큼 미리 잡는다. 컨텍스트 상한이 4096이면 요청마다 4096토큰분의 KV 공간을 연속으로 확보한다.

여기서 두 가지 낭비가 발생한다.

연구에 따르면 이 두 낭비로 실제 활용률이 20~40퍼센트에 머물렀다.

페이징

해결책은 운영체제가 수십 년 전에 찾은 것과 같다. 연속 할당을 포기한다.

KV cache를 고정 크기 블록으로 나눈다. 기본값이 토큰 16개분이다. 요청마다 필요한 만큼만 블록을 할당하고, 블록들은 물리적으로 흩어져 있어도 된다.

각 요청은 블록 테이블을 갖는다. "이 요청의 논리적 위치 0~15는 물리 블록 37번, 16~31은 물리 블록 5번" 하는 식의 매핑이다. 페이지 테이블과 정확히 같은 구조다.

연속 할당과 블록 할당

위는 요청마다 상한만큼 연속으로 잡은 경우다. 실제 사용량 밖의 회색이 전부 낭비이고, 요청이 나간 자리도 조각으로 남는다. 아래는 16토큰짜리 블록으로 필요한 만큼만 잡은 경우다. 물리적으로 흩어져 있어도 블록 테이블이 논리 순서를 복원한다.

attention 커널의 변경

블록이 흩어져 있으면 attention 계산이 곤란해진다. 기존 커널은 KV가 연속이라고 가정하고 메모리를 읽기 때문이다.

그래서 PagedAttention은 블록 테이블을 따라가며 읽는 커널을 함께 제공한다. 이름의 Attention이 여기서 온다. 단순히 메모리 관리 기법이 아니라 커널까지 포함한 것이다.

M8 세션 113에서 말한 대로 이것은 FlashAttention과 직교한다. FlashAttention은 계산 방식을, PagedAttention은 메모리 배치를 다룬다. 실제 엔진은 둘을 결합한 커널을 쓴다.

따라오는 이득

블록 단위 관리는 원래 목표 외에 두 가지를 더 준다.

확인 질문

  1. 연속 할당에서 생기는 두 가지 단편화를 각각 설명해 보라.
  2. 블록 테이블이 무엇이며 어떤 운영체제 개념에 대응하는가?
  3. 블록 단위 관리가 부수적으로 가능하게 하는 것 하나를 말해 보라.
확인

1. 내부 단편화는 요청이 최대 길이만큼 잡아 두고 실제로는 일부만 쓰는 낭비다. 외부 단편화는 요청들이 드나들며 빈 공간이 조각나 총량은 충분해도 연속된 큰 덩어리가 없는 상태다.

2. 요청의 논리적 토큰 위치를 물리 블록 번호로 매핑하는 표다. 운영체제의 페이지 테이블과 같은 구조다.

3. 프리픽스가 같은 요청들이 그 부분의 블록을 공유할 수 있다. 복사 없이 블록 테이블이 같은 물리 블록을 가리키면 된다.

앉아서 읽을 것

vLLM / PagedAttention 논문은 배경 절(2장)만 읽어도 단편화 문제가 얼마나 심각했는지 그림으로 이해된다.


Session 118prefix caching

요청들이 같은 앞부분을 공유하면 그 KV를 재계산하지 않고 재사용한다. 시스템 프롬프트, few-shot 예제, 멀티턴 히스토리, RAG의 문서가 전부 여기 해당하므로 실무에서 효과가 매우 크다.

어디에 공통 프리픽스가 있는가

생각보다 많다.

무엇을 아끼는가

prefill 비용을 아낀다. 프롬프트 2000토큰 중 1800토큰이 캐시에 있으면 200토큰분만 계산하면 된다.

효과는 TTFT에 직접 나타난다. M7 세션 89에서 본 대로 첫 토큰까지의 시간은 prefill이 지배하므로, prefill이 10분의 1이 되면 TTFT도 그만큼 줄어든다.

멀티턴 대화에서는 효과가 극적이다. 10턴째 요청의 프롬프트가 5000토큰이라도 새로 계산할 것은 마지막 질문 몇십 토큰뿐이다.

구현

앞 세션의 블록 구조 덕분에 구현이 자연스럽다.

블록 단위로 해시를 계산한다. 그 블록에 담긴 토큰들과 그 앞의 모든 토큰을 함께 해시하면, 같은 해시는 같은 프리픽스를 뜻한다.

새 요청이 들어오면 앞에서부터 블록 해시를 계산하며 캐시에 있는지 확인한다. 있으면 그 물리 블록을 그대로 가리키고, 없는 지점부터 계산한다.

여기서 prefix라는 이름이 정확한 이유가 나온다. 앞에서부터 연속으로 일치해야 한다. 중간이 다르면 그 뒤는 전부 무효다. attention이 앞의 모든 토큰에 의존하기 때문이다.

프롬프트 설계에 주는 함의

이 메커니즘을 알면 프롬프트를 배치하는 방식이 성능을 좌우한다는 것이 보인다.

확인 질문

  1. prefix caching이 아끼는 것은 무엇이고 어느 지표에 나타나는가?
  2. 프리픽스가 앞에서부터 연속으로 일치해야 하는 이유는?
  3. 시스템 프롬프트 앞에 타임스탬프를 넣으면 무슨 일이 생기는가?
확인

1. prefill 계산을 아끼고 TTFT에 직접 나타난다. 프롬프트의 앞부분이 이미 계산되어 있으면 나머지만 계산하면 된다.

2. attention이 각 토큰의 K, V를 앞의 모든 토큰에 의존해 계산하기 때문이다. 중간이 달라지면 그 뒤의 모든 KV가 무효가 된다.

3. 매 요청 프리픽스가 달라져 캐시가 전혀 맞지 않는다. 전체 프롬프트를 매번 다시 계산하게 되어 TTFT가 크게 나빠진다.


Session 119chunked prefill

긴 prefill을 조각내어 decode 스텝들 사이에 끼워 넣는다. 그러면 한 요청의 긴 prefill이 다른 요청들의 토큰 생성을 멈추게 하는 문제가 사라진다. TTFT와 TPOT 사이의 균형을 잡는 손잡이다.

문제 재확인

세션 116에서 예고한 문제다. continuous batching에서 새 요청이 합류하려면 prefill을 해야 하는데, 그것이 한 스텝을 크게 늘린다.

숫자로 보자. 기존 요청 30개가 decode 중이고 스텝당 20ms가 걸린다. 여기에 프롬프트 4000토큰짜리 요청이 도착한다. 그 prefill이 150ms 걸린다면, 그 스텝은 170ms가 된다.

조각내기

해결책은 prefill을 한 번에 하지 않는 것이다. 4000토큰을 512토큰씩 여덟 조각으로 나누고, 매 스텝 한 조각씩 처리한다.

각 스텝의 구성은 이렇게 된다. 기존 요청 30개의 decode 토큰 30개 + 새 요청의 prefill 조각 512토큰. 합쳐서 542토큰을 한 번에 처리한다.

스텝 시간이 20ms에서 40ms 정도로 늘지만 150ms 멈춤은 사라진다. 새 요청의 prefill은 여덟 스텝에 걸쳐 완료되므로 TTFT는 조금 늘어난다.

트레이드오프

여기서 균형이 명확해진다.

그래서 조각 크기가 서비스 성격에 따라 정해지는 설정값이 된다. 대화형 스트리밍 서비스라면 부드러운 출력이 중요하니 작게, 배치 처리라면 크게 잡는다.

부수 효과

chunked prefill에는 예상 밖의 이득이 있다.

decode만 있는 스텝은 M8 세션 105에서 본 대로 연산 강도가 낮아 GPU가 논다. 여기에 prefill 조각을 섞으면 연산 강도가 올라가 GPU 활용도가 좋아진다.

즉 prefill과 decode를 섞는 것이 둘 다 따로 하는 것보다 효율적이다. 이것을 의도적으로 활용하는 것이 최근 엔진들의 기본 동작이다.

반대 방향 — 분리

흥미롭게도 정반대 접근도 있다. prefill과 decode를 아예 다른 GPU에서 처리하는 것이다.

이유는 둘의 성격이 다르기 때문이다. prefill은 연산 바운드라 연산 성능이 높은 장비가 좋고, decode는 대역폭 바운드라 대역폭이 높은 장비가 좋다. 그리고 각각을 독립적으로 확장할 수 있다. 프롬프트가 긴 시간대에는 prefill 워커를 늘리고, 응답이 긴 시간대에는 decode 워커를 늘린다.

대가는 KV cache를 GPU 사이로 전송해야 한다는 것이다. 고속 인터커넥트가 있는 대규모 클러스터에서 의미가 있고, 최근 엔진들이 이 방식을 지원하기 시작했다.

확인 질문

  1. chunked prefill이 없을 때 긴 프롬프트가 기존 사용자에게 미치는 영향은?
  2. 조각 크기를 작게 할 때와 크게 할 때의 트레이드오프는?
  3. prefill과 decode를 섞으면 GPU 활용도가 좋아지는 이유는?
확인

1. 그 prefill이 한 스텝을 크게 늘려, 같은 배치의 모든 기존 요청이 그만큼 토큰 생성을 멈춘다. 스트리밍 응답이 끊겼다 이어지는 것으로 보인다.

2. 작게 하면 기존 요청의 TPOT가 안정적이지만 새 요청의 TTFT가 늘어난다. 크게 하면 반대다.

3. decode만 있는 스텝은 연산 강도가 낮아 GPU가 논다. prefill 조각을 섞으면 같은 스텝에서 처리하는 토큰 수가 늘어 연산 강도가 올라간다.


Session 120서빙 지표

TTFT, TPOT, ITL, E2E. 네 지표가 서로 다른 것을 재고 서로 다른 수단으로 개선된다. 평균만 보지 않고 분위수로 보는 것도 여기서 정해진다.

정의

무엇이 무엇을 지배하는가

이 대응 관계가 진단의 출발점이다.

지표지배 요인개선 수단
TTFT큐 대기, prefill 길이prefix caching, chunked prefill, 스케줄 우선순위, prefill 분리
TPOTdecode 스텝 시간양자화, 배치 조정, GQA 모델, speculative decoding
ITL 꼬리스텝 시간의 변동chunked prefill, preemption 억제
E2E위 둘 + 출력 길이응답 길이 통제, 프롬프트 개선

마지막 줄이 자주 간과된다. E2E를 가장 크게 줄이는 방법이 응답을 짧게 만드는 것인 경우가 많다. 모델이 불필요하게 장황하면 아무리 엔진을 최적화해도 한계가 있다. M6 세션 78에서 본 길이 편향이 여기서 비용으로 돌아온다.

분위수로 봐야 하는 이유

평균 TTFT가 300ms라도 p95가 3초면 스무 명 중 한 명은 3초를 기다린다. 그리고 그 한 명이 불만을 제기한다.

LLM 서빙에서 꼬리가 긴 이유는 명확하다. 큐 대기 시간의 분산이 크기 때문이다. 트래픽이 몰리면 대기가 급증하고, 긴 프롬프트가 앞에 있으면 뒤가 밀린다.

그래서 SLO는 평균이 아니라 분위수로 정의한다. "p95 TTFT < 1초, p95 TPOT < 50ms" 같은 형태다.

처리량 지표

지연과 별개로 처리량도 잰다.

확인 질문

  1. TTFT와 TPOT는 각각 무엇이 지배하며 개선 수단이 어떻게 다른가?
  2. E2E를 줄이는 방법 중 엔진 최적화가 아닌 것 하나를 말해 보라.
  3. 평균이 아니라 분위수로 SLO를 정의하는 이유는?
확인

1. TTFT는 큐 대기와 prefill이 지배하며 prefix caching·chunked prefill·스케줄 조정으로 개선한다. TPOT는 decode 스텝 시간이 지배하며 양자화·배치·speculative decoding으로 개선한다.

2. 응답 길이를 줄이는 것이다. E2E는 출력 토큰 수에 비례하므로, 불필요하게 장황한 응답을 짧게 만드는 것이 엔진 최적화보다 효과가 클 때가 많다.

3. 큐 대기 시간의 분산이 커서 꼬리가 길기 때문이다. 평균이 좋아도 상위 5퍼센트 사용자가 훨씬 긴 지연을 겪을 수 있다.


Session 121처리량과 지연의 트레이드오프

동시성을 올리면 처리량이 오르다가 어느 지점부터 지연만 늘고 처리량은 정체한다. 그 무릎을 찾는 것이 용량 산정의 핵심이고, 무릎 너머로 밀어붙이면 두 지표가 함께 나빠진다.

곡선의 모양

동시 요청 수를 1부터 올려 가며 처리량과 지연을 재면 전형적인 형태가 나온다.

동시성을 올릴 때

24
목표 SLO 선을 넘지 않는 최대 동시성이 운영 지점이다. 그 너머는 처리량 이득 없이 지연만 늘어난다. 무릎을 찾는 것이 용량 산정의 실체이고, 이 곡선은 모델·프롬프트 길이·응답 길이에 따라 달라지므로 자기 워크로드로 직접 그려야 한다.

무엇을 고를 것인가

두 지표를 동시에 최대화할 수는 없다. 서비스 성격이 선택을 정한다.

실무 절차

무릎을 찾는 방법은 단순하다.

확인 질문

  1. 동시성을 올릴 때 처리량 증가가 둔화되는 두 가지 원인은?
  2. 무릎 너머에서 운영하면 무엇이 나빠지는가?
  3. 부하 테스트에서 자기 워크로드를 써야 하는 이유는?
확인

1. 연산 강도가 GPU 임계값에 도달해 연산이 병목이 되거나, KV cache 예산이 소진되어 더 받을 수 없게 되는 것이다.

2. 처리량은 더 오르지 않는데 큐 대기 시간이 늘어 지연만 급격히 나빠진다.

3. 곡선의 모양이 프롬프트 길이와 응답 길이 분포에 크게 의존하기 때문이다. 합성 벤치마크의 무릎은 실제 서비스의 무릎과 다르다.


Session 122Little's law와 용량 산정

동시요청=도착률×평균응답시간. 이 한 줄이 SLO에서 필요한 GPU 수를 역산하게 해 준다. M8 세션 107의 메모리 계산과 짝을 이룬다.

법칙

대기 행렬 이론의 기본 결과다.

L=λW

L은 시스템 안에 있는 평균 항목 수, λ는 도착률, W는 평균 체류 시간이다. 가정이 거의 없어서 매우 널리 성립한다.

LLM 서빙에 대응시키면 이렇다. L은 동시에 처리 중인 요청 수, λ는 초당 요청 도착 수, W는 요청 하나의 평균 E2E 시간이다.

역산하기

용량 산정은 이 식을 뒤집어 쓴다.

이 계산은 대략적이지만 자릿수를 잡아 준다. 그리고 어느 항목이 병목인지 알려 준다.

어느 쪽을 조일 것인가

계산 결과가 감당 안 되면 세 변수 중 하나를 바꿔야 한다.

두 계산의 관계

M8 세션 107은 메모리가 허용하는 동시성을 준다. 세션 121은 SLO가 허용하는 동시성을 준다. 실제 운영 동시성은 둘 중 작은 쪽이다.

대개는 SLO 쪽이 먼저 걸린다. 메모리상 56개를 받을 수 있어도 그러면 지연이 SLO를 넘기 때문이다. 그래서 메모리를 늘려도 성능이 안 좋아지는 경우가 생기고, 그때는 지연 쪽 최적화가 필요하다.

이 구분을 못 하면 "GPU를 더 샀는데 왜 안 빨라지느냐"는 상황이 된다.

확인 질문

  1. Little's law를 LLM 서빙 용어로 옮겨 보라.
  2. 초당 20요청, 평균 E2E 6초일 때 필요한 동시성은?
  3. 메모리가 허용하는 동시성과 SLO가 허용하는 동시성 중 무엇을 써야 하는가?
확인

1. 동시 처리 요청 수 = 초당 도착 요청 수 × 요청 하나의 평균 E2E 시간이다.

2. 20×6=120개다.

3. 둘 중 작은 쪽이다. 대개 SLO 쪽이 먼저 걸리므로, 메모리를 늘려도 성능이 개선되지 않는 경우가 생긴다.


Session 123추론 엔진 비교

vLLM, SGLang, TensorRT-LLM, llama.cpp. 넷은 다른 것을 최적화하고, 그래서 워크로드 특성이 선택을 정한다. 2026년 현재의 경향을 정리한다.

vLLM

PagedAttention의 원조이고 사실상의 기본값이다.

SGLang

프리픽스 재사용에 특화된 엔진이다.

TensorRT-LLM

NVIDIA의 컴파일 기반 엔진이다.

llama.cpp

CPU와 Apple Silicon을 겨냥한 엔진이다.

선택 절차

정리하면 이렇다.

그리고 벤치마크 숫자를 그대로 믿지 않는다. 워크로드가 다르면 순위가 바뀐다. 자기 프롬프트·응답 길이 분포로 직접 재는 것이 유일하게 신뢰할 만한 방법이다.

확인 질문

  1. SGLang이 우위를 보이는 워크로드의 특성은?
  2. TensorRT-LLM의 강점과 대가를 각각 말해 보라.
  3. 엔진 벤치마크 숫자를 그대로 믿으면 안 되는 이유는?
확인

1. 공유 프리픽스가 많은 워크로드다. RAG, 긴 멀티턴 대화, 에이전트 루프처럼 이전 컨텍스트가 반복되는 경우다.

2. 강점은 컴파일로 최적화된 커널을 만들어 같은 하드웨어에서 최고 성능을 내는 것이다. 대가는 컴파일 시간이 길고 모델·설정을 바꿀 때마다 다시 해야 하며 NVIDIA에 묶인다는 점이다.

3. 성능이 프롬프트 길이, 응답 길이, 프리픽스 공유율에 크게 의존해 워크로드가 다르면 순위가 바뀌기 때문이다.


Session 124스트리밍과 API 계층

토큰을 만들자마자 내보내면 체감 지연이 크게 줄어든다. 그리고 그 앞에 놓이는 게이트웨이 계층에서 인증, 레이트 리밋, 라우팅, 관측이 이루어진다.

왜 스트리밍인가

응답이 300토큰이고 TPOT가 30ms면 완성까지 9초다. 완성을 기다렸다 한 번에 보내면 사용자는 9초를 빈 화면으로 본다.

스트리밍하면 첫 토큰이 TTFT 시점에 나가고, 이후 계속 흘러 나온다. 체감 지연이 9초에서 TTFT 수준으로 바뀐다. 실제 완료 시간은 같은데 경험이 완전히 다르다.

이것이 대화형 LLM 서비스에서 스트리밍이 사실상 필수인 이유다.

구현

가장 널리 쓰이는 것은 SSE다. HTTP 응답을 열어 두고 이벤트를 계속 흘려보낸다. 단방향이면 충분하고 HTTP 인프라를 그대로 쓸 수 있어 WebSocket보다 단순하다.

몇 가지 세부가 실무에서 문제가 된다.

게이트웨이 계층

엔진 앞에 놓이는 계층이 있고, 여기가 백엔드 엔지니어에게 가장 익숙한 영역이다.

확인 질문

  1. 스트리밍이 바꾸는 것은 실제 완료 시간인가 체감 지연인가?
  2. 스트리밍에서 한글이 깨지는 원인과 대응은?
  3. LLM 게이트웨이에서 요청 수만으로 레이트 리밋을 걸면 안 되는 이유는?
확인

1. 체감 지연이다. 완료 시간은 같지만 첫 토큰이 TTFT 시점에 나가 사용자가 훨씬 빨리 반응을 본다.

2. 한글 한 글자가 UTF-8 3바이트라 여러 토큰에 걸칠 수 있다. 토큰마다 즉시 디코딩하지 말고 완성된 문자 경계까지만 내보내고 나머지는 버퍼에 둔다.

3. 요청 수가 적어도 프롬프트가 아주 길면 자원을 독점할 수 있기 때문이다. 토큰 수 기준의 제한이 함께 필요하다.


Session 125분산 추론 개요

모델이 GPU 한 장에 안 들어가면 나눠야 한다. 텐서 병렬은 층 안을 쪼개고, 파이프라인 병렬은 층을 나누고, 데이터 병렬은 복제한다. 셋이 각각 다른 통신 패턴을 만든다.

데이터 병렬

가장 단순하다. 모델 전체를 여러 GPU에 복제하고 요청을 나눠 보낸다.

모델이 한 장에 들어갈 때 쓴다. GPU 사이 통신이 없어 확장이 깔끔하다. 처리량이 GPU 수에 거의 비례해 늘어난다.

한계는 명확하다. 모델이 한 장에 안 들어가면 쓸 수 없고, 개별 요청의 지연은 개선되지 않는다.

텐서 병렬

층 안의 행렬을 여러 GPU에 쪼갠다.

예를 들어 [d, 4d] 행렬을 GPU 두 장에 [d, 2d]씩 나눈다. 각 GPU가 자기 몫을 계산하고, 결과를 합쳐야 다음 층으로 갈 수 있다. 이 합치는 과정이 all-reduce 통신이다.

특징은 이렇다. 층마다 통신이 일어난다. 32층이면 순전파에서 수십 번의 all-reduce가 필요하다. 그래서 GPU 사이 대역폭이 결정적이고, NVLink처럼 빠른 인터커넥트가 있는 노드 안에서만 실용적이다. 노드를 넘어가면 통신이 병목이 된다.

이득은 두 가지다. 모델을 나눠 담을 수 있고, 한 요청의 지연도 줄어든다. 각 GPU가 계산의 일부만 하기 때문이다. 이 두 번째 이득 때문에 모델이 한 장에 들어가도 텐서 병렬을 쓰는 경우가 있다.

파이프라인 병렬

층을 그룹으로 나눠 GPU마다 다른 층을 맡긴다. 32층을 4장에 8층씩 나누는 식이다.

통신이 적다. 그룹 경계에서 활성값만 넘기면 되고, 층마다 통신하는 텐서 병렬과 대조된다. 그래서 노드를 넘어서도 쓸 만하다.

문제는 버블이다. GPU 0이 첫 그룹을 계산하는 동안 나머지 셋은 논다. 마이크로배치로 쪼개 겹치게 하면 완화되지만 완전히 없앨 수는 없다.

조합

실제 대규모 배포는 셋을 섞는다. 노드 안에서는 텐서 병렬(빠른 인터커넥트 활용), 노드 사이는 파이프라인 병렬(통신 적음), 그리고 전체를 데이터 병렬로 복제한다.

언제 필요한가

이 모듈의 다른 내용과 달리 분산 추론은 필요할 때만 쓴다. 판단 기준은 단순하다.

확인 질문

  1. 텐서 병렬과 파이프라인 병렬의 통신 패턴 차이를 말해 보라.
  2. 텐서 병렬이 노드를 넘기 어려운 이유는?
  3. 모델이 한 장에 안 들어갈 때 텐서 병렬보다 먼저 검토할 것은?
확인

1. 텐서 병렬은 층마다 all-reduce가 일어나 통신이 잦다. 파이프라인 병렬은 층 그룹 경계에서만 활성값을 넘기므로 통신이 적다.

2. 층마다 통신이 필요해 총 통신량이 많은데, 노드 사이 대역폭이 노드 안보다 훨씬 낮아 그것이 병목이 되기 때문이다.

3. 양자화다. INT4로 줄이면 한 장에 들어가는 경우가 많고, 통신 없는 단일 GPU 운영이 대개 더 낫다.


Session 126복습 — SLO에서 GPU 수 역산하기

이 모듈의 실전 시험은 이것이다. "TTFT p95 1초, TPOT p95 40ms로 초당 15요청을 처리하려면 무엇이 필요한가"에 논리로 답할 수 있는가.

이어 보기

decode가 대역폭에 막혀 있으므로 배치를 키우는 것이 처리량을 올리는 유일한 실질적 수단이다. 그런데 정적 배칭은 배치 단위가 요청이라 슬롯이 논다. continuous batching은 배치 단위를 스텝으로 낮춰 끝난 요청을 빼고 새 요청을 즉시 넣는다.

이것을 구현하려면 메모리 관리가 필요하다. 순진하게 상한만큼 연속 할당하면 내부·외부 단편화로 활용률이 20~40퍼센트에 머문다. PagedAttention은 고정 크기 블록과 블록 테이블로 이 문제를 없앤다. 부수적으로 프리픽스 공유가 가능해져 prefix caching이 나온다.

새 요청의 긴 prefill이 기존 요청을 멈추게 하는 문제는 chunked prefill로 조각내어 섞는다. 그러면 연산 강도도 함께 올라간다.

지표는 넷이다. TTFT는 큐와 prefill이, TPOT는 decode가 지배하고, 개선 수단이 다르다. 동시성을 올리면 처리량이 오르다 무릎에서 포화되고, 그 너머는 지연만 나빠진다. Little's law로 SLO에서 필요한 GPU 수를 역산하되, 메모리가 허용하는 동시성과 SLO가 허용하는 동시성 중 작은 쪽을 쓴다.

실전 문제

실제로는 응답 길이 통제가 가장 싸다. 그리고 이것은 엔진 설정이 아니라 프롬프트와 모델 선택의 문제다.

자가 점검

  1. 정적 배칭의 낭비가 생기는 근본 원인과 continuous batching의 해법을 말해 보라. (S115, S116)
  2. 연속 할당의 두 단편화와 블록 할당이 그것을 없애는 방식은? (S117)
  3. prefix caching이 효과를 내려면 프롬프트를 어떻게 배치해야 하는가? (S118)
  4. chunked prefill이 푸는 문제와 그 트레이드오프는? (S119)
  5. TTFT와 TPOT의 개선 수단을 각각 두 개씩 대라. (S120)
  6. 동시성의 무릎 너머에서 운영하면 무엇이 나빠지는가? (S121)
  7. Little's law로 초당 15요청·E2E 12초에 필요한 동시성을 계산해 보라. (S122)
  8. 메모리 동시성과 SLO 동시성 중 무엇을 쓰며, 그 구분이 왜 중요한가? (S122)
  9. SGLang을 검토할 만한 워크로드는? (S123)
  10. 모델이 GPU 한 장에 안 들어갈 때의 판단 순서는? (S125)
확인

1. 배치 단위가 요청이라 요청이 끝날 때까지 슬롯이 묶인다. continuous batching은 단위를 디코딩 스텝으로 낮춰 매 스텝 배치를 재구성한다.

2. 내부 단편화(상한만큼 잡고 일부만 씀)와 외부 단편화(빈 공간이 조각남)다. 고정 크기 블록으로 필요한 만큼만 할당하고 블록 테이블로 논리 순서를 복원한다.

3. 변하지 않는 부분(시스템 프롬프트, 예제, 문서)을 앞에, 변하는 부분(사용자 질문)을 뒤에 둔다. 앞부분에 타임스탬프 같은 매번 달라지는 값을 넣지 않는다.

4. 긴 prefill이 한 스텝을 크게 늘려 기존 요청들을 멈추게 하는 문제를 푼다. 조각을 작게 하면 기존 요청의 TPOT가 안정되지만 새 요청의 TTFT가 늘어난다.

5. TTFT는 prefix caching, chunked prefill, 스케줄 우선순위, prefill 분리. TPOT는 양자화, 배치 조정, GQA 모델, speculative decoding.

6. 처리량은 더 오르지 않는데 큐 대기가 늘어 지연만 급격히 나빠진다.

7. 15×12=180개다.

8. 둘 중 작은 쪽을 쓴다. 이 구분을 못 하면 메모리를 늘렸는데 성능이 안 좋아지는 상황을 이해하지 못한다.

9. 공유 프리픽스가 많은 워크로드다. RAG, 긴 멀티턴 대화, 에이전트 루프가 해당한다.

10. 먼저 양자화로 한 장에 넣을 수 있는지 본다. 안 되면 노드 안에서 텐서 병렬을 쓴다. 한 장에 들어가면 데이터 병렬이 거의 항상 낫다.

모듈을 마치며

여기까지가 모델을 돌리는 이야기의 끝이다. M7에서 무슨 일이 일어나는지, M8에서 왜 느린지, M9에서 엔진이 어떻게 푸는지를 봤다.

다음 모듈부터는 모델로 무엇을 만드는가다. 그리고 M10의 검색은 이 트랙에서 백엔드 경험이 두 번째로 크게 전이되는 영역이다. 인덱스와 후보 좁히기, 정밀 검증의 2단계 구조가 그대로 나온다.

M10세션 127–14014개

검색과 RAG

지식을 파라미터가 아니라 컨텍스트로 넣는다. 인덱스로 후보를 좁히고 정밀 검증하는 2단계 구조가 그대로 나오는, 백엔드 경험이 크게 전이되는 모듈이다.

Session 127RAG는 무엇을 푸는 문제인가

모델이 모르는 것을 찾아서 컨텍스트에 넣어 준다. 지식을 파라미터가 아니라 외부에 두는 이 결정이, 갱신 가능성과 출처 추적이라는 두 가지를 한꺼번에 해결한다.

세 가지 문제

M5와 M6에서 확인한 대로 모델의 지식에는 세 가지 한계가 있다.

RAG는 이 셋을 동시에 푼다. 질문이 오면 관련 문서를 검색해 프롬프트에 넣고, 모델에게 그 문서를 근거로 답하게 한다.

구성

가장 단순한 형태는 두 단계다.

말로 하면 간단한데 실제로는 각 단계에 선택이 많고, 그 선택들이 품질을 좌우한다. 이 모듈의 나머지가 그 선택들에 관한 것이다.

파인튜닝과 비교

M6 세션 87에서 정리한 것을 다시 확인한다.

RAG파인튜닝
지식 갱신문서만 바꾸면 됨재학습 필요
출처 표시가능불가능
초기 비용인덱스 구축학습 비용
요청당 비용컨텍스트가 길어져 비쌈추가 비용 없음
잘하는 것지식형식·말투·행동

마지막 줄이 핵심이다. 둘은 경쟁 관계가 아니라 다른 문제를 푼다. 실제 시스템은 형식은 파인튜닝으로, 지식은 RAG로 다루는 조합을 쓴다.

어디서 실패하는가

RAG 시스템의 실패는 두 곳에서 온다.

확인 질문

  1. RAG가 동시에 푸는 세 가지 문제는?
  2. RAG와 파인튜닝이 각각 잘하는 것을 한 마디로 말해 보라.
  3. RAG 시스템의 실패를 두 종류로 나누는 것이 왜 중요한가?
확인

1. 학습 시점 이후의 최신 정보, 학습에 없던 사적·내부 정보, 그리고 답변의 출처 추적이다.

2. RAG는 지식을, 파인튜닝은 형식과 말투와 행동을 다룬다.

3. 원인이 다르면 처방이 다르기 때문이다. 분리하지 않으면 검색 문제를 프롬프트로 고치려 하는 등 잘못된 곳에 노력을 쓴다.


Session 128임베딩 모델은 어떻게 학습되는가

"가깝다"의 정의가 학습 목표에서 정해진다. contrastive 학습으로 관련 있는 쌍은 가깝게, 무관한 쌍은 멀게 만든다. 이 사실을 알면 임베딩이 언제 실패하는지도 보인다.

문제 설정

M3 세션 42에서 본 임베딩 테이블은 토큰 하나의 벡터였다. 문서 검색에 필요한 것은 문장이나 문단 전체를 하나의 벡터로 요약한 것이다.

가장 단순한 방법은 문장을 모델에 통과시키고 나온 토큰 벡터들을 평균 내는 것이다. 그런데 이렇게 만든 벡터는 검색에 잘 맞지 않는다. 언어 모델의 학습 목표가 다음 토큰 예측이지 "관련 문서 찾기"가 아니기 때문이다.

그래서 검색용 임베딩 모델을 따로 학습시킨다.

contrastive 학습

핵심 아이디어는 이렇다. 관련 있는 쌍을 모으고, 그 쌍은 가깝게, 무작위로 뽑은 다른 것들은 멀게 만든다.

학습 데이터는 (질문, 관련 문서) 쌍이다. 검색 로그, 질의응답 데이터, 문서 제목과 본문 같은 것에서 얻는다.

손실 함수는 이런 형태다. 배치 안에 쌍이 여러 개 있을 때, 각 질문에 대해 자기 짝은 가깝게 하고 배치 안의 다른 문서들은 멀게 한다. 이때 다른 문서들이 음성 예제 역할을 한다.

L=logexp(sim(q,d+)/τ)dexp(sim(q,d)/τ)

형태가 낯익을 것이다. M1 세션 17의 cross-entropy와 같은 구조이고, τ는 M7 세션 96의 온도와 같은 역할이다.

어려운 음성 예제

여기서 실무적으로 중요한 세부가 나온다. 무작위 음성 예제만 쓰면 학습이 쉬워서 모델이 대충 배운다. 완전히 무관한 문서와 구별하는 것은 어렵지 않기 때문이다.

그래서 어려운 음성 예제를 넣는다. 주제는 비슷하지만 답을 담고 있지 않은 문서 같은 것이다. 이것들을 밀어내도록 학습하면 모델이 훨씬 정교해진다.

이 사실이 실무에 주는 함의가 있다. 좋은 임베딩 모델은 자기 도메인의 어려운 구별을 학습했어야 한다. 일반 모델이 우리 도메인의 미세한 차이를 구별하지 못하면, 도메인 데이터로 임베딩 모델을 추가 학습하는 것이 큰 효과를 낸다.

비대칭 학습

또 하나 알아 둘 것은 질문과 문서가 다르게 인코딩될 수 있다는 점이다.

질문은 짧고 문서는 길다. 질문은 묻고 문서는 답한다. 성격이 다르므로 모델에게 어느 쪽인지 알려 주는 것이 도움이 된다.

그래서 많은 임베딩 모델이 접두사를 요구한다. 질문에는 query: , 문서에는 passage: 같은 것을 붙이라고 한다.

확인 질문

  1. 일반 언어 모델의 출력을 그대로 검색 임베딩으로 쓰면 잘 안 되는 이유는?
  2. 어려운 음성 예제가 필요한 이유는?
  3. 임베딩 모델이 요구하는 접두사를 빼먹으면 어떻게 되는가?
확인

1. 언어 모델의 학습 목표가 다음 토큰 예측이지 관련 문서 찾기가 아니기 때문이다. "가깝다"의 기준이 검색에 맞게 정해지지 않았다.

2. 무작위 음성 예제는 구별이 너무 쉬워 모델이 대충 배운다. 주제는 비슷하지만 답이 없는 문서를 밀어내도록 학습해야 정교해진다.

3. 성능이 눈에 띄게 떨어지는데 에러가 나지 않아 발견이 늦다. 질문과 문서를 다르게 인코딩하도록 학습된 모델에서 그 신호가 사라지기 때문이다.


Session 129정규화와 거리 함수

M1 세션 7의 결론이 여기서 실무 설정이 된다. 정규화해 두면 내적·코사인·L2가 같은 순위를 준다. 정규화를 빼먹으면 검색이 조용히 망가진다.

세 가지 척도

벡터 검색에서 쓰는 거리 또는 유사도는 셋이다.

정규화가 만드는 등식

M1 세션 7에서 유도한 것을 다시 확인한다. 두 벡터가 모두 L2 정규화되어 있으면,

cos(a,b)=a·b,qquad|ab|2=22(a·b)

세 척도가 같은 순위를 준다. 내적이 크면 코사인도 크고 L2 거리는 작다.

그래서 실무 관행이 정해진다. 임베딩을 저장하기 전에 정규화하고, 인덱스는 계산이 가장 싼 내적으로 설정한다. 대부분의 벡터 데이터베이스가 이 조합을 권장한다.

빼먹으면 생기는 일

정규화를 빼먹고 코사인 인덱스를 쓰면 대개 문제가 없다. 인덱스가 내부적으로 정규화하기 때문이다.

문제는 정규화를 빼먹고 내적 인덱스를 쓸 때다. 벡터 길이가 점수에 섞여 들어가고, 긴 문서나 특정 유형의 텍스트가 부당하게 상위에 온다.

더 흔한 함정은 한쪽만 정규화하는 것이다. 색인할 때는 정규화했는데 질의할 때 빼먹거나 그 반대다. 이 경우 순위가 완전히 망가지지는 않고 미묘하게 틀어진다. 대부분의 검색은 그럭저럭 되고 어려운 질의에서만 실패한다. 그래서 발견이 아주 늦다.

이런 종류의 조용한 실패가 RAG 시스템에서 반복적으로 나타난다. 그래서 인덱스를 만든 뒤 반드시 정확성 검사를 해야 한다. 방법은 다음 세션들에서 다룬다.

확인 질문

  1. 정규화된 벡터에서 세 척도가 같은 순위를 주는 이유를 식으로 설명해 보라.
  2. 실무에서 임베딩을 정규화해 저장하고 내적 인덱스를 쓰는 이유는?
  3. 색인과 질의 중 한쪽만 정규화했을 때의 증상은?
확인

1. 길이가 1이면 코사인의 분모가 1이라 내적과 같고, ab2=22(a·b)이므로 L2 거리는 내적의 단조 감소 함수다.

2. 세 척도가 같은 결과를 주므로 계산이 가장 싼 내적을 안심하고 쓸 수 있기 때문이다.

3. 순위가 완전히 망가지지 않고 미묘하게 틀어진다. 대부분의 검색은 그럭저럭 되고 어려운 질의에서만 실패해서 발견이 매우 늦다.


Session 130차원의 저주와 근사 검색

고차원에서는 모든 점이 비슷하게 멀어진다. 그리고 정확한 최근접 검색은 사실상 전수 조사와 다르지 않다. 그래서 정확성을 일부 포기하고 속도를 얻는 근사 검색으로 간다.

정확한 검색의 비용

문서가 백만 개이고 벡터가 1024차원이라고 하자. 질문 하나에 대해 가장 가까운 열 개를 찾으려면, 백만 번의 내적을 계산해 정렬해야 한다. 내적 하나가 1024번의 곱셈이니 총 10억 번의 연산이다.

한 번은 감당할 만하다. 그런데 초당 수백 질의가 오면 감당이 안 된다. 그리고 문서가 천만 개, 일억 개로 늘어나면 선형으로 늘어난다.

왜 인덱스를 만들기 어려운가

일차원 데이터라면 정렬해 두고 이진 탐색을 하면 된다. 이차원이면 격자로 나눌 수 있다.

그런데 차원이 높아지면 이런 공간 분할이 무너진다. 공간을 차원마다 두 조각으로 나누면 조각 수가 2^d가 되는데, d가 1024면 우주의 원자 수보다 많다. 데이터가 아무리 많아도 대부분의 조각이 비어 있다.

더 근본적인 문제도 있다. 고차원에서는 점들 사이의 거리가 서로 비슷해진다. 가장 가까운 점과 가장 먼 점의 거리 차이가 상대적으로 줄어든다. 그러면 "가까운 것만 보고 나머지는 건너뛴다"는 가지치기가 효과를 잃는다.

이 현상들을 통틀어 차원의 저주라고 부른다.

근사로 전환

그래서 발상을 바꾼다. 정확히 가장 가까운 열 개를 찾는 것을 포기하고, 대체로 가까운 열 개를 빠르게 찾는다.

정답 열 개 중 아홉 개를 찾고 하나를 놓쳤다면 recall@10이 0.9다. 검색 품질이 조금 떨어지는 대신 속도가 수백 배 빨라진다.

인지해야 할 것

다만 근사라는 사실을 잊으면 안 된다. RAG가 답을 못 찾을 때 원인 후보가 하나 더 생기는 것이다.

원인은 세 층에 있을 수 있다. 청킹이 잘못되어 답이 조각 경계에서 잘렸거나, 임베딩이 그 질문과 문서를 가깝게 놓지 않았거나, ANN이 실제로는 가까운 문서를 놓쳤거나.

세 번째를 확인하는 방법은 단순하다. brute-force로 정확한 답을 구해 비교한다. 문서가 많으면 느리지만 진단용으로는 충분하다. 정확 검색으로는 찾히는데 ANN으로는 안 찾힌다면 인덱스 파라미터 문제다.

이 확인을 하지 않으면 임베딩 모델을 바꾸거나 청킹을 다시 하는 데 시간을 쓰게 된다.

확인 질문

  1. 고차원에서 공간 분할 인덱스가 무너지는 이유 두 가지를 말해 보라.
  2. 근사 검색으로 전환하는 것이 정당화되는 이유는?
  3. RAG가 답을 못 찾을 때 ANN이 원인인지 확인하는 방법은?
확인

1. 차원마다 나누면 조각 수가 2^d로 폭발해 대부분이 비고, 고차원에서는 점들 사이 거리가 서로 비슷해져 가지치기가 효과를 잃는다.

2. 임베딩 유사도 자체가 진짜 관련성의 근사이므로, 그 위에서 정확한 1등을 찾는 것에 큰 의미가 없기 때문이다. 뒤에 리랭커가 있으면 더욱 그렇다.

3. brute-force로 정확한 최근접을 구해 비교한다. 정확 검색으로는 찾히는데 ANN으로 안 찾히면 인덱스 파라미터 문제다.


Session 131ANN 알고리즘 — IVF와 PQ

두 가지 다른 발상이다. IVF는 후보를 좁히고, PQ는 벡터를 압축한다. 흔히 함께 쓰이고, 각각 다른 자원을 절약한다.

IVF — 군집으로 좁히기

발상은 단순하다. 벡터들을 미리 군집으로 나눠 둔다. 예를 들어 백만 개를 천 개의 군집으로 나누면 군집마다 평균 천 개다.

각 군집에는 중심점이 있다. 질의가 오면 먼저 천 개의 중심점과만 비교해 가까운 군집 몇 개를 고른다. 그다음 그 군집 안의 벡터들만 정밀 비교한다.

군집 열 개를 본다면 비교 횟수가 1000+10000=11000번이다. 백만 번에서 백분의 일 이하로 줄었다.

PQ — 벡터 압축

전혀 다른 문제를 푼다. 메모리다.

1024차원 float32 벡터 하나가 4KB다. 일억 개면 400GB다. 메모리에 올릴 수 없다.

PQ는 벡터를 조각으로 나눠 각 조각을 코드북의 인덱스로 대체한다. 1024차원을 128차원씩 8조각으로 나누고, 각 조각마다 256개의 대표 벡터를 미리 학습해 둔다. 그러면 각 조각을 1바이트로 표현할 수 있고, 벡터 하나가 8바이트가 된다. 4KB에서 8바이트로 500배 압축이다.

거리 계산도 빨라진다. 코드북의 대표 벡터들과 질의의 거리를 미리 계산해 표로 만들어 두면, 각 벡터와의 거리를 덧셈 여덟 번으로 근사할 수 있다.

대가는 정확도다. 압축이므로 원래 거리와 오차가 생긴다.

조합

실무에서는 둘을 함께 쓰는 경우가 많다. IVF로 후보를 좁히고, 그 안에서 PQ로 압축된 벡터로 빠르게 비교한다. 이름이 IVF-PQ 같은 형태로 나타난다.

여기에 재정렬 단계를 붙이기도 한다. PQ로 상위 백 개를 뽑은 뒤, 그 백 개만 원본 벡터로 정확히 비교해 순위를 다시 매긴다. 원본 벡터를 디스크에 두고 필요할 때만 읽으면 메모리를 아끼면서 정확도를 회복할 수 있다.

선택

확인 질문

  1. IVF와 PQ가 각각 절약하는 자원은 무엇인가?
  2. IVF에서 recall과 속도를 맞바꾸는 손잡이는 무엇인가?
  3. PQ의 정확도 손실을 회복하는 방법은?
확인

1. IVF는 비교 횟수, 즉 계산 시간을 절약한다. PQ는 벡터 저장에 필요한 메모리를 절약한다.

2. 탐색할 군집의 개수다. 많이 보면 recall이 오르고 느려진다.

3. 재정렬이다. PQ로 상위 후보를 뽑은 뒤 그것들만 원본 벡터로 정확히 비교해 순위를 다시 매긴다.


Session 132HNSW

계층 그래프를 만들어, 위층에서 성큼성큼 이동하고 아래층에서 정밀 탐색한다. 스킵 리스트를 고차원으로 옮긴 구조이고, 현재 가장 널리 쓰이는 ANN 알고리즘이다.

근접 이웃 그래프

먼저 계층 없는 버전을 보자. 각 벡터를 노드로 두고, 서로 가까운 노드들을 간선으로 연결한다. 노드마다 이웃이 M개쯤 되게 만든다.

검색은 이렇게 한다. 아무 노드에서 출발해 이웃들을 보고, 질의에 더 가까운 이웃으로 이동한다. 더 가까운 이웃이 없으면 멈춘다. 이것을 탐욕적 탐색이라고 한다.

이 방법은 대체로 잘 되지만 두 문제가 있다. 지역 최솟값에 빠질 수 있고, 먼 곳에서 출발하면 이동 횟수가 많다.

계층 추가

HNSW는 계층을 도입해 두 번째 문제를 푼다.

노드마다 확률적으로 층을 배정한다. 대부분은 최하층에만 있고, 일부가 위층에도 올라간다. 위층으로 갈수록 노드가 희박하고, 간선은 더 먼 거리를 잇는다.

검색은 최상층에서 시작한다. 노드가 적으므로 몇 번의 이동으로 대략적인 위치를 찾는다. 그다음 한 층 내려가 더 정밀하게, 다시 내려가 더 정밀하게. 최하층에 도달하면 정밀 탐색을 한다.

이 구조가 스킵 리스트와 정확히 같다. 위층에서 성큼 이동하고 아래층에서 세밀하게 찾는다.

위층에서 성큼, 아래층에서 정밀하게

별이 질의, 진한 점이 현재 위치다. 위층은 노드가 희박해 한 번의 이동으로 멀리 간다. 아래로 내려갈수록 촘촘해져 정밀하게 좁힌다. efSearch를 키우면 각 층에서 후보를 더 많이 유지해 recall이 오르고 속도가 떨어진다.

파라미터

세 개가 중요하다.

마지막 성질이 실무적으로 중요하다. efSearch질의마다 다르게 줄 수 있다. 중요한 질의는 크게, 일반 질의는 작게 해서 품질과 비용을 조절할 수 있다.

장단점

백엔드의 언어로

HNSW는 다층 스킵 리스트다. 위층에서 대략적인 위치를 잡고 아래층에서 정밀 탐색하는 구조가 같다.

그리고 efSearch옵티마이저 힌트에 해당한다. 같은 질의를 더 철저히 탐색하도록 지시하는 값이고, recall과 지연을 맞바꾼다. 실행 계획을 조절해 본 경험이 그대로 쓰인다.

확인 질문

  1. HNSW의 계층 구조가 푸는 문제는 무엇인가?
  2. efConstructionefSearch의 차이는?
  3. HNSW에서 삭제가 까다로운 이유는?
확인

1. 단일 층 그래프에서 먼 곳에서 출발하면 목표까지 이동 횟수가 많다는 문제다. 희박한 위층에서 성큼 이동해 대략적인 위치를 먼저 잡는다.

2. efConstruction은 그래프를 만들 때만 쓰이고 인덱스 품질을 정한다. efSearch는 검색할 때마다 쓰이고 런타임에 질의별로 바꿀 수 있다.

3. 노드를 지우면 그것을 경유하던 그래프 연결이 끊어져 탐색이 실패할 수 있기 때문이다. 대개 삭제 표시만 하고 주기적으로 재구축한다.

앉아서 읽을 것

HNSW 논문은 그림 하나(계층 탐색 그림)만 봐도 구조가 이해된다. faiss와 pgvector의 문서에서 파라미터 권장값을 확인해 두면 실무에 바로 쓴다.


Session 133recall과 QPS의 트레이드오프

ANN 인덱스에는 정확도라는 손잡이가 있고, 그것을 돌리면 속도가 반대로 움직인다. 이 곡선을 자기 데이터에서 직접 그려 봐야 어디에 설정할지 정할 수 있다.

무엇을 재는가

이 둘은 반비례한다. 더 철저히 탐색하면 정확해지고 느려진다.

곡선 그리기

절차는 이렇다.

이 곡선을 보면 판단이 쉬워진다. 대개 recall 0.9 근처까지는 QPS 손실이 작고, 0.95를 넘어가면서 급격히 비싸진다. 어디까지 지불할 것인가를 곡선을 보고 정한다.

얼마가 필요한가

recall이 얼마나 되어야 충분한가. 이것은 뒤에 무엇이 오는가에 달려 있다.

이것이 다음 세션들에서 리랭커를 다루는 이유이기도 하다. 2단계 구조가 검색 파라미터에 여유를 준다.

인덱스 빌드 비용

또 하나 잴 것이 있다. 인덱스를 만드는 시간과 메모리다.

문서가 수백만 개면 HNSW 빌드에 시간이 오래 걸린다. efConstruction을 키우면 품질이 좋아지지만 빌드가 몇 배 느려진다.

그리고 증분 갱신이 되는지가 중요하다. 문서가 매일 추가되는데 매번 전체를 다시 만들어야 한다면 운영이 어렵다. HNSW는 삽입이 되지만 삭제가 까다롭다는 점을 세션 132에서 봤다.

실무에서는 대개 이렇게 한다. 주 인덱스를 두고, 새 문서는 작은 보조 인덱스에 넣어 두 인덱스를 함께 검색하고, 주기적으로 병합한다. 로그 구조 병합과 같은 발상이다.

확인 질문

  1. recall@k를 계산하려면 무엇이 먼저 필요한가?
  2. 리랭커가 있으면 ANN의 recall 요구가 낮아지는 이유는?
  3. 문서가 계속 추가되는 환경에서 인덱스를 어떻게 운영하는가?
확인

1. brute-force로 구한 정확한 상위 k개, 즉 정답 집합이 필요하다.

2. ANN이 넓게 top-100을 뽑고 리랭커가 그중 top-5를 고르므로, top-100 안에만 정답이 있으면 된다. recall@100은 recall@10보다 훨씬 쉽게 높아진다.

3. 주 인덱스와 새 문서용 보조 인덱스를 함께 검색하고 주기적으로 병합한다. 로그 구조 병합과 같은 방식이다.


Session 134BM25와 역색인

어휘 일치 검색은 낡은 기술이 아니다. 고유명사, 코드, 제품 ID, 오타 앞에서 임베딩보다 강하고, 그래서 지금도 하이브리드의 한 축으로 남아 있다.

역색인

단어마다 그 단어가 등장하는 문서 목록을 저장한다. "연차"라는 단어를 찾으면 그것이 나온 문서들을 즉시 얻는다.

정확 일치라 빠르고, 인덱스 크기가 예측 가능하며, 갱신이 쉽다. 수십 년 동안 검색의 기본 구조였다.

BM25

역색인으로 후보를 찾은 뒤 점수를 매기는 함수다. 세 가지 직관이 들어 있다.

세부 수식은 실무에서 외울 필요가 없다. 드문 단어의 일치를 크게 치고, 반복은 포화시키고, 길이를 보정한다는 세 원칙만 알면 된다.

어디서 임베딩보다 나은가

반대로 임베딩이 강한 곳은 표현이 다른데 의미가 같은 경우다. "휴가 규정"으로 물었는데 문서에는 "연차 사용 지침"이라고 쓰여 있는 경우다. BM25는 이것을 못 찾는다.

한국어에서의 어려움

BM25는 단어 단위로 동작하므로 형태소 분석이 필요하다.

한국어는 조사가 붙는다. "연차를", "연차가", "연차는"이 전부 다른 문자열이다. 분석 없이 그대로 색인하면 "연차"로 검색해도 못 찾는다.

그래서 형태소 분석기를 쓰거나, 문자 n-gram으로 색인한다. 후자는 분석기 없이도 되고 미등록어에 강하지만 인덱스가 커지고 오탐이 는다.

이 대목은 관계형 데이터베이스에서 한국어 전문 검색을 다뤄 본 경험이 그대로 쓰이는 지점이다.

확인 질문

  1. BM25의 세 가지 직관을 말해 보라.
  2. 임베딩보다 BM25가 나은 경우를 두 가지 들어 보라.
  3. 한국어에서 BM25를 쓸 때의 어려움과 대응은?
확인

1. 질의어 빈도가 높으면 관련도가 높되 포화한다, 드문 단어의 일치가 더 중요하다, 긴 문서는 우연한 일치가 많으므로 보정한다.

2. 제품 코드나 고유명사처럼 정확한 문자열이 중요한 경우, 그리고 임베딩 모델이 학습에서 거의 못 본 드문 전문 용어다.

3. 조사가 붙어 같은 단어가 다른 문자열이 되므로 형태소 분석이 필요하다. 분석기를 쓰거나 문자 n-gram으로 색인하는데, 후자는 미등록어에 강하지만 인덱스가 크고 오탐이 는다.


Session 135하이브리드 검색과 RRF

두 검색이 서로 다른 것을 잘하므로 합치면 둘 다 얻는다. 문제는 점수 체계가 달라 직접 더할 수 없다는 것이고, 그 해법이 순위 기반 융합이다.

왜 합치는가

앞 세션에서 본 대로 BM25와 임베딩은 강점이 다르다. 실제 질의 집합을 보면 어떤 질의는 한쪽이, 어떤 질의는 다른 쪽이 이긴다.

그리고 중요한 관찰이 있다. 두 방법이 놓치는 문서가 서로 다르다. 그래서 합치면 각각보다 나은 결과가 나온다. 실무에서 하이브리드가 거의 항상 단일 방법보다 좋은 이유다.

점수를 더할 수 없는 이유

순진한 방법은 두 점수를 가중합하는 것이다. 그런데 문제가 있다.

BM25 점수는 범위가 정해져 있지 않다. 질의와 코퍼스에 따라 5일 수도 40일 수도 있다. 코사인 유사도는 −1에서 1 사이다. 스케일이 완전히 다르다.

정규화해도 해결되지 않는다. 질의마다 점수 분포가 달라서, 어떤 질의에서는 BM25 최고점이 30이고 다른 질의에서는 8이다. 각 질의 안에서 정규화하면 상대 강도 정보가 사라진다.

RRF

해결책은 점수를 버리고 순위만 쓰는 것이다.

RRF(d)=rretrievers1k+rankr(d)

각 검색기에서 그 문서의 순위를 가져와, k(보통 60)를 더한 값의 역수를 더한다.

1위면 1/61, 2위면 1/62, 10위면 1/70이다. 상위 순위에 가중치가 실리되 급격히 떨어지지는 않는다. 그리고 두 검색기 모두에서 상위권인 문서가 가장 높은 점수를 받는다.

장점이 분명하다. 점수 스케일을 신경 쓸 필요가 없고, 튜닝할 파라미터가 사실상 없으며, 검색기를 셋 이상으로 늘려도 그대로 작동한다.

실무 구성

전형적인 파이프라인은 이렇다.

가중치를 주고 싶다면

RRF에 검색기별 가중치를 붙일 수도 있다. 도메인에 따라 한쪽이 확실히 나은 경우가 있기 때문이다.

다만 이 가중치를 정하려면 평가셋이 필요하다. 감으로 정하면 오히려 나빠질 수 있다. M12에서 만들 golden set으로 검증하는 것이 순서다.

기본값(가중치 없음)으로 시작해서, 평가셋에서 개선이 확인될 때만 조정하는 것이 안전하다.

확인 질문

  1. BM25 점수와 코사인 유사도를 직접 더할 수 없는 이유는?
  2. RRF의 계산 방식과 그 장점을 말해 보라.
  3. 하이브리드가 단일 방법보다 나은 근본 이유는?
확인

1. 점수의 스케일이 다르고, BM25는 범위가 정해져 있지 않으며 질의마다 분포가 달라 정규화로도 해결되지 않는다.

2. 각 검색기에서의 순위에 상수를 더한 값의 역수를 합산한다. 점수 스케일을 무시할 수 있고 튜닝할 파라미터가 거의 없으며 검색기를 늘려도 그대로 작동한다.

3. 두 방법이 놓치는 문서가 서로 다르기 때문이다. 어휘 일치와 의미 일치가 상호 보완적이라 합집합이 각각보다 넓다.


Session 136청킹 전략

문서를 어떻게 자르는가가 검색 품질을 크게 좌우한다. 작으면 문맥이 잘리고 크면 신호가 흐려진다. 정답은 데이터마다 다르므로 측정으로 정해야 한다.

왜 자르는가

이유는 셋이다.

크기의 트레이드오프

자르는 방식

오버랩

인접한 조각이 조금씩 겹치게 하는 기법이다. 보통 10에서 20퍼센트다.

경계에 걸친 정보를 놓치지 않게 해 준다. 답이 두 조각의 경계에 있으면 어느 쪽도 온전히 담지 못하는데, 오버랩이 있으면 한쪽이 담는다.

대가는 저장 공간과 중복이다. 같은 내용이 두 조각에 들어가면 검색 결과에 중복이 뜬다.

문맥 보강

작은 조각의 문맥 손실을 다르게 푸는 방법이 있다.

마지막 방법이 특히 유용하다. 정밀한 검색과 충분한 문맥을 동시에 얻는다.

어떻게 정하는가

정답이 없으므로 측정한다.

이 실험은 하루면 되고, 이후 모든 것의 기반이 된다. 청킹을 대충 정하고 시작하면 나중에 인덱스를 다시 만들어야 한다.

확인 질문

  1. 조각이 너무 클 때와 너무 작을 때의 문제를 각각 말해 보라.
  2. 구조가 있는 문서에서 구조 기반 청킹이 나은 이유는?
  3. "검색 단위와 제공 단위를 분리한다"는 것이 무슨 뜻인가?
확인

1. 크면 여러 주제가 섞여 임베딩이 흐려지고 컨텍스트를 많이 차지한다. 작으면 문맥이 잘려 대명사나 참조의 의미가 사라진다.

2. 제목 계층이나 함수 경계 같은 구조가 이미 의미 단위와 일치하기 때문이다. 기계적으로 자르는 것보다 의미가 온전히 보존된다.

3. 검색은 정밀도를 위해 작은 조각으로 하되, 컨텍스트에 넣을 때는 그 조각의 앞뒤를 함께 넣어 충분한 문맥을 준다는 뜻이다.


Session 137reranker가 필요한 이유

bi-encoder는 따로 인코딩하고 cross-encoder는 함께 인코딩한다. 이 차이가 속도와 정확도를 가르고, 그래서 ANN으로 넓게 좁히고 리랭커로 정밀하게 고르는 2단계가 표준이 된다.

두 구조의 차이

대가는 정확도다. 문서를 인코딩할 때 어떤 질문이 올지 모르므로, 질문과 문서의 상호작용을 볼 수 없다. 문서 전체를 하나의 벡터로 압축하면서 질문에 따라 달라져야 할 정보가 뭉개진다.

정확도가 훨씬 높다. 대가는 속도다. 미리 계산할 수 없다. 질문이 와야 계산이 시작되고, 문서마다 모델을 한 번씩 돌려야 한다. 백만 개 문서에는 쓸 수 없다.

2단계 구조

그래서 조합한다.

이 구조가 주는 이득이 크다. 세션 133에서 본 대로 1단계의 recall 요구가 낮아진다. top-100 안에만 있으면 되므로 ANN 파라미터를 공격적으로 잡아 속도를 얻을 수 있다.

무엇이 좋아지는가

리랭커의 효과는 실제로 크다. 특히 이런 경우다.

비용

리랭커는 공짜가 아니다. 후보 100개에 대해 모델을 돌리면 수십에서 수백 밀리초가 추가된다. TTFT에 그대로 더해진다.

그래서 후보 수를 조절한다. 20개면 빠르고 100개면 정확하다. 이것도 측정해서 정할 문제다.

확인 질문

  1. bi-encoder와 cross-encoder의 구조적 차이와 그 결과를 말해 보라.
  2. 리랭커가 있으면 ANN 파라미터를 공격적으로 잡을 수 있는 이유는?
  3. 리랭커가 특히 효과적인 질의 유형을 하나 들어 보라.
확인

1. bi-encoder는 질문과 문서를 따로 인코딩해 문서를 미리 계산할 수 있어 빠르지만 상호작용을 볼 수 없다. cross-encoder는 둘을 함께 넣어 attention으로 직접 비교하므로 정확하지만 미리 계산할 수 없어 느리다.

2. 최종 순위를 리랭커가 정하므로 1단계는 정답이 후보 집합 안에 있기만 하면 된다. recall@100은 recall@10보다 쉽게 높아진다.

3. 주제는 같은데 답이 없는 문서를 걸러야 하는 경우, 조건이 여럿인 질문, 부정이 포함된 질문이다.


Session 138컨텍스트 구성과 lost in the middle

찾은 것을 어떻게 배치하는가도 성능 변수다. 모델은 컨텍스트 중간의 정보를 덜 활용하는 경향이 있고, 그래서 순서와 분량이 결과를 바꾼다.

관측된 현상

같은 문서 집합을 컨텍스트에 넣되 정답 문서의 위치만 바꿔 가며 실험하면, 정답이 앞이나 뒤에 있을 때보다 중간에 있을 때 성능이 떨어진다. U자 곡선이 나온다.

이유는 완전히 규명되지 않았지만, 위치 인코딩의 성질과 학습 데이터의 구조(중요한 정보가 앞이나 뒤에 오는 경향)가 함께 작용하는 것으로 보인다.

최근 모델들은 이 현상이 완화되었지만 없어지지는 않았다. 특히 컨텍스트가 길수록 다시 나타난다.

순서 정하기

여기서 실무 규칙이 나온다.

무엇을 넣지 않을 것인가

이 세션의 진짜 주제다. 컨텍스트 엔지니어링은 무엇을 넣을지보다 무엇을 넣지 않을지의 문제다.

컨텍스트는 유한한 자원이고, 넣은 것들이 서로 주의를 경쟁한다. 관련 없는 문서 하나가 모델을 잘못된 방향으로 유도할 수 있다.

그래서 리랭커 점수에 임계값을 두는 것이 유용하다. 상위 k개를 무조건 넣는 대신, 점수가 기준 미만이면 아예 넣지 않는다. 관련 문서가 없으면 컨텍스트가 비고, 그러면 모델이 "관련 정보를 찾지 못했다"고 답할 수 있다. 이것이 억지로 답을 지어내는 것보다 낫다.

인용과 근거

RAG의 큰 장점이 출처 표시인데, 이것도 설계가 필요하다.

프리픽스 캐시와의 관계

M9 세션 118에서 본 것이 여기서 적용된다.

검색된 문서를 프롬프트 앞쪽에 두면 프리픽스가 되지만, 문서 순서가 질의마다 달라지면 캐시가 안 맞는다.

그래서 선택이 생긴다. 관련도 순으로 정렬해 품질을 얻을 것인가, 안정된 순서로 캐시 적중률을 얻을 것인가. 자주 검색되는 문서 집합이 있다면 후자의 이득이 클 수 있다.

실무에서는 시스템 프롬프트와 고정 지침을 맨 앞에, 검색 결과를 그 뒤에 두는 절충이 흔하다. 최소한 고정 부분은 캐시된다.

확인 질문

  1. lost in the middle 현상이 무엇이며 실무 규칙으로 어떻게 이어지는가?
  2. 검색된 문서를 많이 넣을수록 좋지 않은 이유는?
  3. 문서 순서를 관련도 순으로 정렬할 때 생기는 성능상의 대가는?
확인

1. 정답 정보가 컨텍스트 중간에 있을 때 앞이나 뒤에 있을 때보다 활용도가 떨어지는 현상이다. 가장 관련 있는 문서를 앞이나 뒤에 배치하라는 규칙으로 이어진다.

2. 관련 없는 문서가 신호 대 잡음비를 떨어뜨리고 모델을 잘못된 방향으로 유도할 수 있기 때문이다. 리랭커 점수에 임계값을 두는 편이 낫다.

3. 질의마다 순서가 달라져 프리픽스 캐시가 맞지 않는다. TTFT가 나빠진다.


Session 139GraphRAG

벡터 검색은 한 조각 안에 있는 답을 잘 찾는다. 여러 문서에 흩어진 관계를 종합해야 하는 질문에는 약하고, 그것을 그래프로 푸는 것이 GraphRAG다.

벡터 RAG가 못 하는 질문

"김 부장이 담당한 프로젝트들의 총 예산은?" 같은 질문을 생각해 보자.

김 부장이 담당한 프로젝트 목록이 한 문서에, 각 프로젝트의 예산이 다른 문서들에 있다면, 벡터 검색은 이 질문과 유사한 조각을 찾을 수 없다. 질문 자체를 담은 문서가 없기 때문이다.

그래프로 만들기

GraphRAG의 절차는 이렇다.

대가

강력하지만 비싸다.

언제 쓰는가

판단 기준은 질문의 성격이다.

그리고 둘을 함께 쓰는 것이 실무적으로 흔하다. 질문 유형을 분류해 단순한 것은 벡터로, 복합적인 것은 그래프로 보낸다.

더 가벼운 대안

GraphRAG 전체를 도입하지 않고도 비슷한 효과를 얻는 방법들이 있다.

확인 질문

  1. 벡터 RAG가 약한 질문 유형 두 가지를 말해 보라.
  2. GraphRAG의 주된 비용 두 가지는?
  3. GraphRAG 전에 시도할 만한 가벼운 대안을 하나 들어 보라.
확인

1. 여러 문서에 흩어진 정보를 연결해야 답이 나오는 질문과, 전체를 요약하거나 경향을 파악해야 하는 전역적 질문이다.

2. 모든 문서에 LLM을 돌려 엔티티를 추출하는 구축 비용과, 문서 추가 시 그래프를 갱신하고 엔티티 병합을 다시 판단해야 하는 유지 비용이다.

3. 구조화 가능한 정보를 메타데이터로 저장해 필터링에 쓰는 것이다. 복합 질문을 여러 단순 질문으로 분해하는 방법도 있다.


Session 140복습 — "검색이 안 됩니다"

이 모듈의 실전 시험은 하나다. RAG가 답을 못 찾을 때 원인을 세 계층으로 분리해 진단할 수 있는가.

이어 보기

RAG는 지식을 파라미터가 아니라 컨텍스트에 둔다. 그래서 갱신 가능하고 출처를 밝힐 수 있다. 파인튜닝과 경쟁하지 않고 다른 문제를 푼다.

검색용 임베딩은 contrastive 학습으로 만들어지고, "가깝다"의 정의가 그 학습 목표에서 정해진다. 벡터를 정규화해 두면 내적·코사인·L2가 같은 순위를 준다. 고차원에서는 정확한 최근접 검색이 사실상 전수 조사라서 근사 검색으로 간다. IVF는 후보를 좁히고 PQ는 벡터를 압축하며, HNSW는 계층 그래프로 위층에서 성큼 아래층에서 정밀하게 찾는다. efSearch가 recall과 속도를 맞바꾸는 손잡이다.

어휘 일치인 BM25는 고유명사·코드·드문 용어에서 여전히 강하다. 두 방법이 놓치는 것이 다르므로 RRF로 순위를 융합한다. 청킹은 크기와 방식을 측정으로 정하고, 검색 단위와 제공 단위를 분리하는 패턴이 유용하다. bi-encoder로 넓게 좁히고 cross-encoder 리랭커로 정밀하게 고르는 2단계가 표준이며, 이 구조가 1단계의 recall 요구를 낮춘다.

컨텍스트는 유한한 자원이라 무엇을 넣지 않을지가 중요하고, 중간에 묻히는 현상 때문에 순서도 변수다. 여러 문서를 연결해야 하는 질문에는 GraphRAG를 검토하되 가벼운 대안을 먼저 시도한다.

3계층 진단

"이 질문에 답을 못 찾습니다"를 받으면 순서대로 확인한다.

먼저 brute-force로 확인한다. 정확 검색으로 상위에 오면 ANN 파라미터 문제다. efSearch를 올린다.

정확 검색으로도 안 오면 임베딩 문제다. 질문과 문서가 임베딩 공간에서 멀다. 접두사를 빠뜨렸는지, 정규화를 빼먹었는지 먼저 확인하고, 그다음 임베딩 모델 자체를 의심한다. 고유명사나 코드가 문제라면 BM25를 섞는다.

리랭커가 있다면 리랭커가 떨어뜨렸는지도 본다. 1단계 후보에는 있었는데 리랭커가 하위로 보냈을 수 있다.

자가 점검

  1. 임베딩 모델의 접두사를 빼먹으면 왜 발견이 늦은가? (S128)
  2. 임베딩을 정규화해 저장하는 실무 이유는? (S129)
  3. 고차원에서 정확 검색을 포기하는 것이 정당화되는 이유는? (S130)
  4. IVF와 PQ가 각각 절약하는 자원은? (S131)
  5. HNSW에서 efSearchefConstruction과 다른 점은? (S132)
  6. 리랭커가 있으면 ANN recall 요구가 낮아지는 이유는? (S133, S137)
  7. BM25 점수와 코사인 유사도를 직접 더할 수 없는 이유와 그 해법은? (S135)
  8. "검색 단위와 제공 단위를 분리한다"의 뜻과 이점은? (S136)
  9. 검색된 문서를 많이 넣을수록 좋지 않은 이유는? (S138)
  10. "답을 못 찾는다"의 원인을 세 계층으로 나누고 각각의 확인 방법을 말해 보라. (전체)
확인

1. 에러가 나지 않고 검색이 그럭저럭 작동하기 때문이다. 성능만 눈에 띄지 않게 떨어져서 원인을 다른 곳에서 찾게 된다.

2. 내적·코사인·L2가 같은 순위를 주게 되어, 인덱스 종류를 바꿔도 결과가 같고 계산이 가장 싼 내적을 쓸 수 있다.

3. 임베딩 유사도 자체가 관련성의 근사이므로 그 위에서 정확한 1등을 찾는 데 큰 의미가 없다. 뒤에 리랭커가 있으면 더욱 그렇다.

4. IVF는 비교 횟수(계산 시간), PQ는 벡터 저장 메모리를 절약한다.

5. efConstruction은 빌드 시점에만 영향을 주고, efSearch는 런타임에 질의마다 바꿀 수 있다.

6. 최종 순위를 리랭커가 정하므로 1단계는 정답이 후보 집합 안에만 있으면 된다. recall@100은 recall@10보다 훨씬 쉽게 높아진다.

7. 점수 스케일이 다르고 질의마다 분포도 달라 정규화로도 해결되지 않는다. 해법은 점수를 버리고 순위만 쓰는 RRF다.

8. 작은 조각으로 검색해 정밀도를 얻고, 컨텍스트에는 그 앞뒤를 함께 넣어 문맥을 준다. 정밀 검색과 충분한 문맥을 동시에 얻는다.

9. 관련 없는 문서가 신호 대 잡음비를 떨어뜨리고 모델을 오도할 수 있다. 컨텍스트는 유한 자원이고 넣은 것들이 주의를 경쟁한다.

10. 인덱스에 있는가(문자열 검색으로 확인), 검색이 찾는가(brute-force와 비교해 ANN·임베딩·리랭커 중 어디인지 특정), 모델이 쓰는가(컨텍스트에 들어갔는데 답이 틀린 경우).

모듈을 마치며

이 모듈에서 반복해서 나온 패턴이 하나 있다. 값싼 방법으로 후보를 좁히고 비싼 방법으로 정밀 검증한다. IVF와 재정렬, ANN과 리랭커, PQ와 원본 비교가 전부 같은 구조다.

이 패턴은 데이터베이스에서 인덱스로 후보를 좁히고 조건으로 정밀 필터링하던 것과 정확히 같다. 이름과 자료구조가 바뀌었을 뿐이다.

다음 모듈은 모델에게 도구를 쥐여 준다. 검색도 그 도구 중 하나가 되고, 모델이 스스로 여러 번 검색하며 답을 찾아 나가는 구조로 확장된다.

M11세션 141–15010개

에이전트와 도구

모델은 함수를 실행하지 않는다. 토큰을 낼 뿐이고 실행은 전적으로 내 코드가 한다. 이 사실에서 신뢰 경계와 실패 처리의 설계가 전부 따라 나온다.

Session 141tool calling은 실제로 어떻게 동작하는가

모델은 함수를 실행하지 않는다. 특정 형식의 토큰을 낼 뿐이고, 그것을 파싱해 실제로 실행하는 것은 전적으로 우리 코드다. 이 사실을 놓치면 보안 경계를 잘못 긋는다.

실제 경로

"도구를 쓴다"는 표현이 마법처럼 들리지만 실제로는 문자열을 주고받는 것이다. 순서대로 따라가 보자.

학습된 행동이다

모델이 이 형식을 지키는 것은 사후학습으로 배운 것이다. M6에서 본 SFT 데이터에 도구 호출 예제가 들어 있었고, 그래서 그 형식으로 출력하는 습관이 생겼다.

여기서 두 가지가 따라 나온다.

보안 경계

이 세션의 실무적 핵심이다. 모델의 출력은 신뢰할 수 없는 입력이다.

모델이 delete_user(id=123)을 생성했다고 해서 실행해서는 안 된다. 그것은 사용자의 요청도, 시스템의 결정도 아니고 확률적으로 생성된 문자열이다. 그리고 뒤에서 볼 프롬프트 인젝션으로 공격자가 그 생성을 유도할 수도 있다.

그래서 실행 코드가 반드시 해야 할 일이 있다.

이것은 사용자 입력을 다루던 것과 똑같은 규율이다. 다만 입력의 출처가 사람이 아니라 모델이라는 점만 다르다.

확인 질문

  1. 모델이 도구를 "실행한다"는 표현이 부정확한 이유는?
  2. 모델이 도구 호출 형식을 지키는 것은 어디서 온 능력인가?
  3. 모델이 생성한 도구 호출을 신뢰할 수 없는 입력으로 다뤄야 하는 이유는?
확인

1. 모델은 정해진 형식의 토큰을 생성할 뿐이고, 그것을 파싱해 실제 함수를 호출하는 것은 우리 코드다.

2. 사후학습이다. SFT 데이터에 도구 호출 예제가 포함되어 그 형식으로 출력하는 습관이 학습되었다.

3. 확률적으로 생성된 문자열이고, 프롬프트 인젝션으로 공격자가 그 생성을 유도할 수도 있기 때문이다. 사용자 입력과 같은 규율로 검증해야 한다.


Session 142스키마와 structured output

도구 호출은 형식이 깨지면 시스템이 멈춘다. M7 세션 100의 제약 디코딩, 스키마 검증, 제한된 재시도의 3중 구조로 보장한다.

스키마 설계

도구 설명은 모델이 읽는 문서다. 그래서 사람이 읽을 API 문서를 쓰듯이 써야 한다.

3중 보장

재시도 메시지

재시도할 때 무엇을 알려 주느냐가 성공률을 좌우한다.

추론 여지 주기

M7 세션 100에서 언급한 것을 여기서 구체화한다.

곧바로 도구 호출만 내라고 강제하면 모델이 생각할 여지가 없어 잘못된 호출이 는다. 스키마에 추론 필드를 먼저 두면 개선된다.

`` { "reasoning": "사용자가 김철수의 연차 잔여일을 물었으므로 직원 검색이 먼저 필요하다", "tool": "search_employee_by_name", "args": { "name": "김철수" } } ``

reasoning을 앞에 두면 모델이 그것을 생성하면서 상황을 정리하고, 그 결과를 조건으로 도구를 고른다. 순서가 중요하다. 뒤에 두면 이미 정해진 선택을 사후 정당화할 뿐이다.

확인 질문

  1. 도구 스키마에서 enum을 쓰는 것이 왜 유용한가?
  2. 3중 보장의 각 단계가 무엇을 담당하는가?
  3. 추론 필드를 도구 호출 앞에 두는 이유는?
확인

1. 값이 정해진 목록으로 제한되어 모델이 임의의 값을 만들어 낼 여지가 사라진다. 제약 디코딩과 결합하면 잘못된 값이 구조적으로 불가능해진다.

2. 제약 디코딩이 문법을 보장하고, 스키마 검증이 값의 타당성을 확인하고, 제한된 재시도가 마지막 안전망이 된다.

3. 모델이 추론을 생성하면서 상황을 정리하고 그 결과를 조건으로 도구를 고르게 된다. 뒤에 두면 이미 정해진 선택을 사후 정당화할 뿐이다.


Session 143agent loop

에이전트는 생성 → 도구 실행 → 결과 주입을 반복하는 루프다. 프레임워크가 감싸고 있지만 안을 열면 상태 머신과 예산 관리가 전부다.

최소 루프

가장 단순한 형태는 이렇다.

대화 기록을 모델에 보낸다. 모델이 응답을 생성한다. 그 응답이 도구 호출이면 실행하고 결과를 기록에 추가한 뒤 다시 모델에 보낸다. 도구 호출이 아니면 최종 답변이므로 루프를 끝낸다.

이것이 ReAct라고 불리는 패턴의 뼈대다. 추론과 행동을 번갈아 하기 때문에 붙은 이름이다.

다른 형태들

실무에서는 대개 ReAct의 변형으로 시작한다. 단순하고 예측 가능하기 때문이다.

상태 머신으로 보기

프레임워크 없이 구현하면 구조가 명확해진다. 다음을 명시적으로 관리하면 된다.

이렇게 두면 어디서 무엇이 잘못됐는지 추적할 수 있고, 재시도와 재개도 쉬워진다.

프레임워크는 무엇을 하는가

프레임워크를 쓰면 위 구조를 직접 짜지 않아도 된다. 대신 몇 가지를 알아 둬야 한다.

그래서 권장되는 순서는 이렇다. 한 번은 직접 구현해 보고, 구조를 이해한 뒤 프레임워크를 쓴다. 그러면 프레임워크가 무엇을 대신해 주고 무엇을 숨기는지 알게 된다.

확인 질문

  1. ReAct 루프의 세 단계를 말해 보라.
  2. agent loop를 상태 머신으로 보면 관리해야 할 것 네 가지는?
  3. 프레임워크를 쓸 때 먼저 확인해야 할 것 하나를 말해 보라.
확인

1. 모델 생성, 도구 실행, 결과를 대화 기록에 주입. 도구 호출이 없으면 종료한다.

2. 상태, 전이 조건, 예산(스텝·토큰·시간·비용), 그리고 지금까지의 기록이다.

3. 프레임워크가 자체적으로 넣는 프롬프트가 무엇인지, 그리고 실제로 나가는 프롬프트를 로깅으로 볼 수 있는지다.


Session 144종료 조건과 루프 방지

에이전트의 가장 흔한 실패는 틀린 답이 아니라 끝나지 않는 것이다. 예산을 명시하지 않으면 비용이 통제 불능이 된다.

왜 안 끝나는가

예산

가장 확실한 방어는 상한을 거는 것이다. 여러 축으로 건다.

루프 감지

예산만으로는 부족하다. 15스텝을 다 쓰고 실패하는 것보다 3스텝에서 루프를 감지해 끊는 것이 낫다.

개입 방법은 두 가지다. 컨텍스트에 알린다. "같은 도구를 같은 인자로 이미 호출했습니다. 다른 접근을 시도하거나 현재 정보로 답하세요." 이렇게 하면 모델이 방향을 바꾸는 경우가 많다. 그래도 안 되면 강제 종료한다.

명시적 종료

종료를 모델의 자발적 판단에 맡기지 않는 방법도 있다. 완료를 선언하는 도구를 하나 만드는 것이다.

finish(answer)라는 도구를 두고, 이것을 호출해야 종료되게 한다. 그러면 종료가 명시적인 행동이 되어 모델이 더 확실히 판단한다. 그리고 최종 답변이 구조화된 형태로 나와 검증하기 쉽다.

부분 실패 처리

도구 하나가 실패했다고 전체가 실패할 필요는 없다.

마지막이 중요하다. 실패를 숨기고 그럴듯한 답을 만드는 것이 최악이다. 모델에게 도구 실패를 명확히 알려 주면 대개 그 사실을 답변에 반영한다.

백엔드의 언어로

여기 나온 것들이 전부 익숙한 개념이다. 예산은 타임아웃과 서킷 브레이커, 루프 감지는 중복 요청 탐지, 부분 실패 처리는 부분 성공 응답이다. 그리고 도구 재시도에서 멱등성을 따지는 것도 같다.

확인 질문

  1. 에이전트가 끝나지 않는 원인 두 가지를 말해 보라.
  2. 예산을 여러 축으로 거는 이유는?
  3. 도구가 실패했을 때 모델에게 그 사실을 알려야 하는 이유는?
확인

1. 같은 도구를 같은 인자로 반복 호출하거나, 두 접근을 번갈아 시도하며 오가는 경우다. 과제가 애초에 불가능한 경우도 있다.

2. 하나만 걸면 다른 축에서 폭주할 수 있기 때문이다. 스텝 수는 적어도 각 스텝의 토큰이 크거나, 시간이 짧아도 비용이 클 수 있다.

3. 알리지 않으면 모델이 결과가 있는 것처럼 그럴듯한 답을 지어낼 수 있다. 실패를 명시하면 대개 그 사실을 답변에 반영한다.


Session 145컨텍스트 예산 경쟁

에이전트가 진행될수록 컨텍스트가 계속 자란다. 도구 결과가 쌓이고 대화가 길어진다. 무엇을 버릴 것인가가 곧 품질이 된다.

자라는 구조

에이전트 루프의 특징은 매 스텝 이전 기록 전체를 다시 넣는다는 것이다.

10스텝짜리 과제에서 각 스텝의 도구 결과가 2000토큰이면, 마지막 스텝의 프롬프트는 2만 토큰이 넘는다. 여기에 시스템 프롬프트와 도구 설명이 더해진다.

세 가지 문제가 생긴다.

무엇을 버릴 것인가

무엇을 지킬 것인가

버리면 안 되는 것도 있다.

그래서 압축은 최근 것과 중요한 것은 유지하고 중간의 세부를 줄이는 방향으로 한다.

구조화된 상태

더 나은 접근이 있다. 모든 것을 대화 기록에 쌓는 대신, 별도의 구조화된 상태를 유지하는 것이다.

지금까지 확정된 사실을 필드가 있는 객체로 관리하고, 매 스텝 그것을 요약해 프롬프트에 넣는다. 대화 기록은 최근 몇 스텝만 유지한다.

이렇게 하면 컨텍스트가 무한정 자라지 않고, 상태를 검사하고 검증하기도 쉬워진다. 에이전트를 대화가 아니라 상태 기계로 보는 관점이 여기서 실질적인 이득을 준다.

확인 질문

  1. 에이전트에서 컨텍스트가 자라면 생기는 문제 세 가지는?
  2. 도구 결과를 자를 때 프롬프트가 아니라 도구에서 자르는 것이 나은 이유는?
  3. 구조화된 상태를 별도로 유지하는 방식의 이점은?
확인

1. 매 스텝 전체를 다시 처리하는 비용, prefill이 길어지는 지연, 그리고 노이즈 증가와 중간 정보 활용도 저하로 인한 품질 하락이다.

2. 도구에서 자르면 애초에 컨텍스트에 들어가지 않으므로 그 이후 모든 스텝에서 비용과 노이즈가 발생하지 않는다.

3. 컨텍스트가 무한정 자라지 않고, 확정된 사실을 검사하고 검증하기 쉬워진다.


Session 146메모리 — 단기·요약·장기

세 층이 서로 다른 문제를 푼다. 단기는 지금 대화, 요약은 길어진 대화, 장기는 세션을 넘는 정보다. 각 층의 실패 방식도 다르다.

단기 메모리

현재 컨텍스트에 들어 있는 것이다. 대화 기록과 도구 결과.

가장 정확하다. 원문이 그대로 있으므로 정보 손실이 없다. 대신 유한하고 비싸다.

요약 메모리

대화가 길어지면 오래된 부분을 요약해 압축한다.

대응은 무엇을 반드시 보존할지 미리 정하는 것이다. 숫자, 고유명사, 결정 사항 같은 것은 요약에서 원문 그대로 유지하도록 지시한다. 규칙 기반 추출과 LLM 요약을 결합하는 편이 안전하다.

장기 메모리

세션을 넘어 유지되는 정보다. 사용자의 선호, 과거 대화의 결론, 프로필 같은 것.

구현은 대개 저장소에 넣고 필요할 때 검색해 오는 방식이다. 즉 M10의 RAG와 같은 구조이고, 검색 품질 문제가 그대로 따라온다.

무엇을 저장할 것인가

장기 메모리는 자동으로 쌓기보다 명시적으로 정하는 편이 안전하다.

반대로 모든 대화를 자동으로 저장하는 것은 위험하다. 노이즈가 쌓이고, 사생활 문제가 생기며, 잘못된 회상이 늘어난다.

확인 질문

  1. 세 층의 메모리가 각각 무엇을 다루는가?
  2. 요약 메모리의 실패 방식과 대응은?
  3. 장기 메모리를 자동으로 쌓는 것이 위험한 이유는?
확인

1. 단기는 현재 컨텍스트의 대화와 도구 결과, 요약은 길어진 대화의 압축본, 장기는 세션을 넘어 유지되는 선호와 사실이다.

2. 손실 압축이라 나중에 필요한 세부가 사라진다. 숫자·고유명사·결정 사항 같은 것은 원문 그대로 보존하도록 지시하고 규칙 기반 추출을 병행한다.

3. 노이즈가 쌓여 잘못된 회상이 늘고, 사생활 문제가 생기며, 오래된 정보가 현재 대화를 오염시킨다.


Session 147MCP

도구를 붙이는 방식을 표준화한 프로토콜이다. 무엇이 표준화되고 무엇은 여전히 각자의 몫인지를 구분하는 것이 실무에서 중요하다.

문제

도구를 붙일 때마다 같은 작업을 반복하게 된다. 스키마를 정의하고, 프롬프트에 넣을 형식으로 변환하고, 호출을 파싱하고, 실행하고, 결과를 포맷팅한다. 그리고 이 코드가 프레임워크마다, 모델 제공자마다 조금씩 다르다.

도구가 열 개이고 프레임워크가 세 개면 서른 벌의 통합 코드가 필요하다. 이 조합 폭발이 문제다.

표준화하는 것

MCP는 도구 제공자와 모델 사용자 사이의 인터페이스를 정의한다.

도구를 제공하는 쪽은 서버를 만든다. 어떤 도구가 있고 각각의 스키마가 무엇인지 노출하고, 호출을 받아 실행한다.

모델을 쓰는 쪽은 클라이언트가 된다. 서버에 연결해 도구 목록을 받아 오고, 필요할 때 호출한다.

그러면 도구를 한 번 만들면 여러 클라이언트에서 쓸 수 있고, 클라이언트를 한 번 만들면 여러 도구를 붙일 수 있다. N × M이 N + M이 된다.

도구 외에도 리소스(읽을 데이터)와 프롬프트 템플릿 같은 것을 표준화한다.

표준화되지 않는 것

이 구분이 중요하다.

실무 판단

확인 질문

  1. MCP가 푸는 조합 폭발 문제를 설명해 보라.
  2. MCP가 표준화하지 않는 것 두 가지를 말해 보라.
  3. 외부 MCP 서버를 붙일 때 주의할 점은?
확인

1. 도구가 N개, 클라이언트가 M개면 통합 코드가 N×M벌 필요하다. 표준 인터페이스가 있으면 각각 한 번씩만 구현하면 되어 N+M이 된다.

2. 어떤 도구를 만들지, 스키마 설명을 어떻게 쓸지, 권한 검증을 어떻게 할지, 에이전트 루프를 어떻게 설계할지는 여전히 각자의 몫이다.

3. 외부 코드이므로 반환 내용과 도구 설명 모두 신뢰할 수 없는 입력으로 다뤄야 한다. 접근 범위를 명확히 제한하고 로그를 남긴다.


Session 148프롬프트 인젝션

공격은 사용자 입력만이 아니라 검색된 문서와 도구 결과를 통해 들어온다. 모델은 지시와 데이터를 구조적으로 구별하지 못하고, 그래서 방어는 코드 계층에서 해야 한다.

근본 문제

전통적인 인젝션 공격은 데이터가 코드로 해석되면서 생긴다. SQL 인젝션이 그렇다. 방어는 데이터와 코드를 구조적으로 분리하는 것이었다. 파라미터 바인딩을 쓰면 데이터가 아무리 SQL처럼 생겨도 코드로 해석되지 않는다.

LLM에는 그런 분리가 없다. 시스템 프롬프트도 사용자 입력도 검색된 문서도 전부 같은 토큰 배열이다. 모델은 "이 부분은 지시이고 저 부분은 데이터"라는 구조적 구별을 하지 못한다. 특수 토큰으로 역할을 표시하지만 그것은 학습된 경향일 뿐 강제가 아니다.

원리적으로 완전한 방어가 없다. 이 사실을 인정하는 것이 출발점이다.

어디로 들어오는가

뒤쪽 세 개가 훨씬 위험하다. 사용자를 신뢰해도 소용없고, 공격 표면이 넓기 때문이다.

무엇을 노리는가

방어

완전한 방어가 없으므로 여러 층으로 위험을 줄인다.

백엔드의 언어로

이것은 신뢰 경계 문제다. 어디까지가 신뢰할 수 있는 영역이고 어디부터가 아닌지를 긋고, 경계를 넘는 데이터를 검증한다.

다른 점은 전통적 시스템에서는 파싱 단계에서 구조적으로 분리할 수 있었는데 여기서는 그것이 불가능하다는 것이다. 그래서 입력 검증이 아니라 출력 검증과 권한 축소가 주된 방어가 된다. 무엇이 들어오는지 통제할 수 없으니 무엇이 나가는지를 통제한다.

확인 질문

  1. LLM에서 프롬프트 인젝션의 완전한 방어가 원리적으로 어려운 이유는?
  2. 사용자 입력보다 검색 문서나 도구 결과를 통한 공격이 더 위험한 이유는?
  3. 가장 효과적인 방어 하나와 그 이유를 말해 보라.
확인

1. 시스템 프롬프트, 사용자 입력, 검색 문서가 전부 같은 토큰 배열이라 모델이 지시와 데이터를 구조적으로 구별하지 못하기 때문이다. SQL의 파라미터 바인딩 같은 분리가 없다.

2. 사용자가 선의여도 공격이 성립하고, 인덱스나 외부 페이지 등 공격 표면이 훨씬 넓기 때문이다.

3. 권한 축소다. 에이전트가 할 수 있는 일이 적으면 뚫려도 피해가 제한된다. 입력을 완전히 통제할 수 없으므로 결과의 영향 범위를 줄이는 것이 가장 확실하다.


Session 149결정론과 확률론의 경계

이 트랙 전체의 결론에 해당하는 세션이다. 규칙으로 쓸 수 있는 것은 전부 코드로 강제하고, 판단이 필요한 것만 모델에 맡기되 그 출력도 결정론적으로 검증한다.

원칙

M0 세션 5에서 예고한 것을 여기서 확정한다.

LLM을 잘 쓰는 시스템은 모델에게 많이 맡기는 시스템이 아니라 맡길 곳과 맡기지 않을 곳을 정확히 나눈 시스템이다.

기준은 단순하다. 규칙으로 쓸 수 있는가.

쓸 수 있으면 코드로 쓴다. 정확하고, 빠르고, 싸고, 테스트할 수 있고, 감사할 수 있다.

쓸 수 없으면 모델에 맡긴다. 자연어 이해, 의도 파악, 요약, 여러 근거를 종합한 판단 같은 것이다.

구체적인 분할

권한 검사. 사용자가 이 자원에 접근할 수 있는지는 규칙이다. 모델에게 물어보지 않는다.

금액 계산. 산술은 코드가 정확하다.

형식 강제. M7 세션 100의 제약 디코딩.

데이터 접근 범위. SQL을 생성시키더라도 실행 전에 화이트리스트로 검증한다.

멱등성과 재시도. 상태 변경의 안전성은 코드가 보장한다.

자연어 질문을 구조화된 질의로 바꾸기.

여러 문서에서 답을 종합하기.

애매한 요청의 의도 파악하기.

결과를 사람이 읽을 형태로 설명하기.

검증을 끼워 넣기

모델의 출력을 그대로 쓰지 않고 결정론적 검증을 통과시킨다.

생성된 SQL은 실행 전에 파싱해 허용된 테이블과 컬럼만 쓰는지 확인한다. 생성된 JSON은 스키마로 검증한다. 인용한 문서 번호는 실제로 존재하는지 확인한다. 계산된 값은 코드로 다시 계산해 대조한다.

이 검증이 실패하면 재시도하거나 실패로 처리한다. 검증 없이 나가는 경로를 만들지 않는다.

어디에 두는가

같은 기능도 어느 계층에서 구현하느냐에 따라 성질이 다르다. 형식 준수를 예로 들면 네 가지 선택지가 있다.

판단 연습

몇 가지 상황을 놓고 생각해 보자.

확인 질문

  1. 코드와 모델을 나누는 기준을 한 문장으로 말해 보라.
  2. 형식 준수를 보장하는 네 가지 방법을 확실성 순으로 나열해 보라.
  3. "이 사용자가 이 문서를 볼 수 있는가"를 모델에게 묻지 않는 이유는?
확인

1. 규칙으로 쓸 수 있으면 코드로 강제하고, 판단이 필요해 규칙화가 어려운 것만 모델에 맡기되 그 출력도 결정론적으로 검증한다.

2. 프롬프트로 부탁 → SFT로 학습 → 제약 디코딩으로 강제 → 출력 검증 후 거부. 아래로 갈수록 확실하고 비싸다.

3. 권한은 명확한 규칙이고 확률적 판단에 맡길 이유가 없다. 그리고 모델의 판단은 프롬프트 인젝션으로 조작될 수 있다.


Session 150복습 — 에이전트 설계와 실패 모드

이 모듈의 실전 시험은 이것이다. 에이전트 하나를 설계하면서 실패 모드 다섯 개를 미리 나열하고 각각의 대응을 말할 수 있는가.

이어 보기

도구 호출은 마법이 아니다. 모델이 정해진 형식의 토큰을 내고, 우리 코드가 파싱해 실행하고, 결과를 컨텍스트에 넣는다. 그래서 모델의 출력은 신뢰할 수 없는 입력이고 사용자 입력과 같은 규율로 다뤄야 한다.

형식은 프롬프트로 부탁하지 않고 제약 디코딩 + 스키마 검증 + 제한된 재시도로 보장한다. 스키마에 추론 필드를 앞에 두면 모델이 생각할 여지가 생긴다.

에이전트는 생성·실행·주입의 루프이고, 상태 머신과 예산 관리로 보면 명확해진다. 가장 흔한 실패는 틀린 답이 아니라 끝나지 않는 것이라, 스텝·토큰·시간·비용의 상한을 걸고 루프를 감지한다. 컨텍스트는 계속 자라므로 무엇을 버릴지를 설계해야 하고, 구조화된 상태를 별도로 두는 편이 낫다.

메모리는 세 층이고 각각 다른 방식으로 실패한다. 프롬프트 인젝션은 사용자 입력만이 아니라 검색 문서와 도구 결과를 통해 들어오며, 원리적으로 완전한 방어가 없어 권한 축소와 출력 검증이 주된 방어가 된다.

그리고 이 모든 것의 바탕에 하나의 원칙이 있다. 규칙으로 쓸 수 있는 것은 전부 코드로.

설계 연습

사내 문서 질의 에이전트를 설계한다고 하자. 도구는 문서 검색, 직원 조회, 계산기 셋이다.

이 다섯 개를 설계 시점에 나열하는 것이 이 모듈의 목표다. 만들고 나서 발견하는 것과 미리 대비하는 것의 차이가 크다.

자가 점검

  1. 모델이 도구를 "실행한다"는 표현이 부정확한 이유와, 그것이 보안에 주는 함의는? (S141)
  2. structured output 3중 보장의 각 단계는? (S142)
  3. 추론 필드를 도구 호출 앞에 두는 이유는? (S142)
  4. 에이전트에 예산을 여러 축으로 거는 이유는? (S144)
  5. 도구 실패를 모델에게 숨기면 안 되는 이유는? (S144)
  6. 도구 결과를 프롬프트가 아니라 도구에서 잘라야 하는 이유는? (S145)
  7. 장기 메모리의 두 가지 실패 방식은? (S146)
  8. 프롬프트 인젝션의 완전한 방어가 원리적으로 어려운 이유는? (S148)
  9. 인젝션 방어에서 가장 효과적인 수단과 그 이유는? (S148)
  10. 코드와 모델의 경계를 긋는 기준을 한 문장으로 말해 보라. (S149)
확인

1. 모델은 형식화된 토큰을 낼 뿐이고 실행은 우리 코드가 한다. 따라서 그 출력은 확률적으로 생성된 신뢰할 수 없는 입력이며, 인자 검증·권한 확인·범위 제한·감사 로그가 전부 우리 책임이다.

2. 제약 디코딩(문법 보장), 스키마 검증(값의 타당성), 제한된 재시도(마지막 안전망)다.

3. 모델이 추론을 생성하며 상황을 정리하고 그 결과를 조건으로 도구를 고르게 되기 때문이다. 뒤에 두면 이미 정해진 선택의 사후 정당화가 된다.

4. 하나만 걸면 다른 축에서 폭주할 수 있다. 스텝은 적은데 각 스텝의 토큰이 크거나, 시간은 짧은데 비용이 클 수 있다.

5. 알리지 않으면 모델이 결과가 있는 것처럼 그럴듯한 답을 지어낸다. 명시하면 대개 그 사실을 답변에 반영한다.

6. 도구에서 자르면 애초에 컨텍스트에 들어가지 않아 이후 모든 스텝의 비용과 노이즈가 줄어든다.

7. 관련 없는 과거 정보를 가져와 현재를 오염시키는 잘못된 회상, 그리고 새 정보가 옛 정보와 모순될 때의 갱신 충돌이다.

8. 시스템 프롬프트·사용자 입력·검색 문서가 전부 같은 토큰 배열이라 모델이 지시와 데이터를 구조적으로 구별할 수 없기 때문이다.

9. 권한 축소다. 입력을 완전히 통제할 수 없으므로, 뚫렸을 때의 영향 범위를 줄이는 것이 가장 확실하다.

10. 규칙으로 쓸 수 있는 것은 전부 코드로 강제하고, 판단이 필요한 것만 모델에 맡기되 그 출력도 결정론적 검증을 통과시킨다.

모듈을 마치며

여기까지가 시스템을 만드는 이야기다. 남은 것은 그것이 실제로 잘 동작하는지 어떻게 아는가이다.

그리고 이 트랙에서 가장 자주 건너뛰어지면서 가장 중요한 부분이 그것이다. M0 세션 5에서 말한 사고 전환이 마지막 모듈에서 구체적인 공학이 된다. 열세 번의 이동이 남았다.

M12세션 151–16313개

평가와 운영

"잘 되는 것 같다"를 데이터로 바꾸는 공학이다. 이 트랙에서 가장 자주 건너뛰어지면서 가장 중요한 부분이고, 관문 G4로 끝난다.

Session 151LLM 시스템의 품질은 무엇인가

측정하지 않은 개선은 개선이 아니다. 그런데 무엇을 측정할지가 자명하지 않다는 것이 LLM 시스템의 특수한 어려움이고, 그것을 정의하는 것이 평가 설계의 첫 단계다.

왜 어려운가

일반 소프트웨어의 테스트는 단순하다. 입력을 주고 기대값과 비교한다. 같으면 통과다.

LLM 시스템은 세 가지가 다르다.

그래서 판정 기준을 다시 만들어야 한다.

무엇을 잴 것인가

시스템의 목적에 따라 다르지만 대개 이 축들이 나온다.

무엇부터 시작하는가

전부를 처음부터 재려 하면 시작하지 못한다. 순서가 있다.

원칙

이 모듈 전체를 관통하는 원칙 세 가지를 먼저 적어 둔다.

확인 질문

  1. LLM 시스템의 테스트가 일반 소프트웨어와 다른 점 세 가지는?
  2. 평가 체계를 만들 때 어디서 시작하는 것이 실용적인가?
  3. 최적화 대상과 검증 지표를 분리해야 하는 이유는?
확인

1. 출력이 매번 다르고, 정답이 하나가 아니며, 부분적으로 맞을 수 있어 통과/실패로 나눌 수 없다.

2. 실제 실패 사례를 모아 유형을 분류하고, 가장 자주 나타나는 유형부터 측정 가능하게 만든다. 완벽한 체계보다 자주 실패하는 것을 재는 불완전한 체계가 유용하다.

3. 최적화하는 지표는 반드시 그 방향으로 왜곡되기 때문이다. 독립적인 검증 지표가 없으면 실제 품질이 떨어지는 것을 감지할 수 없다.


Session 152오프라인 평가 데이터셋 만들기

실제 트래픽에서 시작한다. 상상해서 만든 질문은 실제 사용자가 묻는 것과 다르고, 그 차이가 평가를 무의미하게 만든다.

어디서 오는가

무엇을 담는가

각 항목에 최소한 이것들이 필요하다.

어려운 케이스를 포함한다

평가셋이 쉬운 질문만 있으면 모든 버전이 90퍼센트를 넘어 구별이 안 된다. 변별력이 있어야 유용하다.

의도적으로 포함할 것들이 있다.

규모와 관리

확인 질문

  1. 합성 질문만으로 평가셋을 만들면 안 되는 이유는?
  2. "답이 없는 질문"을 평가셋에 넣어야 하는 이유는?
  3. 평가셋을 개발용과 최종 검증용으로 나누는 이유는?
확인

1. 실제 사용자 질문의 애매함, 오타, 불완전한 문장을 재현하지 못해 분포가 실제와 다르기 때문이다.

2. 시스템이 정보가 없을 때 "모른다"고 답하는지 확인해야 하는데, 그런 항목이 없으면 환각을 측정할 방법이 없다.

3. 평가셋을 보며 고치면 그 평가셋에 과적합된다. 최종 검증용을 따로 두고 자주 보지 않아야 일반화 성능을 알 수 있다.


Session 153retrieval과 generation 분리 평가

합친 점수만 보면 어디를 고쳐야 하는지 알 수 없다. RAG 평가의 첫 번째 규칙은 검색과 생성을 따로 재는 것이다.

왜 분리하는가

최종 답변 정확도가 70퍼센트라고 하자. 이 숫자만으로는 아무것도 할 수 없다.

검색이 정답 문서를 90퍼센트 찾는데 생성이 그중 78퍼센트만 제대로 쓰는 것일 수도 있고, 검색이 75퍼센트만 찾는데 생성은 찾은 것의 93퍼센트를 제대로 쓰는 것일 수도 있다. 처방이 완전히 다르다.

앞이면 프롬프트와 컨텍스트 구성을 고쳐야 하고, 뒤면 인덱스와 임베딩을 고쳐야 한다.

어떻게 분리하는가

두 값의 차이가 검색이 만든 손실이다. 이상적 컨텍스트에서 95퍼센트인데 실제로는 70퍼센트라면 검색이 병목이다.

진단표

세 숫자를 놓고 보면 판단이 명확해진다.

검색 recall이상적 컨텍스트 정확도실제 정확도진단
높음높음낮음컨텍스트 구성 문제 (순서, 노이즈, 개수)
높음낮음낮음생성 문제 (프롬프트, 모델)
낮음높음낮음검색 문제 (인덱스, 임베딩, 청킹)
낮음낮음낮음둘 다 — 검색부터 고친다

첫 줄이 놓치기 쉬운 경우다. 검색도 잘하고 생성도 잘하는데 합치면 안 되는 상황. 원인은 대개 M10 세션 138에서 본 컨텍스트 구성이다. 관련 없는 문서가 섞였거나 정답이 중간에 묻혔다.

에이전트 시스템에서는

M11의 에이전트가 붙으면 단계가 더 늘어난다. 각각을 따로 재는 원칙은 같다.

각 단계의 성공률을 따로 재면 어디서 무너지는지 보인다. 종단간 성공률만 보면 개선할 곳을 찾을 수 없다.

확인 질문

  1. 검색과 생성을 분리 평가하지 않으면 무엇이 불가능해지는가?
  2. "이상적인 컨텍스트로 평가"가 측정하는 것은 무엇인가?
  3. 검색도 좋고 이상적 컨텍스트 정확도도 높은데 실제 정확도가 낮다면 원인은?
확인

1. 어디를 고쳐야 할지 판단하는 것이다. 같은 종단간 점수라도 검색 문제일 수도 생성 문제일 수도 있고 처방이 완전히 다르다.

2. 검색의 영향을 제거한 순수한 생성 능력이다. 정답 문서를 직접 넣어 준 상태에서의 정확도다.

3. 컨텍스트 구성 문제다. 관련 없는 문서가 섞였거나, 정답이 중간에 묻혔거나, 문서 수가 너무 많다.


Session 154검색 지표

recall@k, precision@k, MRR, nDCG. 각각 다른 것에 민감하고, 시스템 구조에 따라 무엇을 봐야 하는지가 달라진다.

recall@k

상위 k개 안에 정답 문서가 들어 있는 비율이다.

계산이 단순하고 해석이 명확해서 가장 먼저 봐야 할 지표다.

precision@k

상위 k개 중 관련 있는 문서의 비율이다.

MRR

정답이 처음 나타나는 위치의 역수를 평균한 것이다. 1위면 1, 2위면 0.5, 10위면 0.1이다.

nDCG

가장 정교하다. 두 가지를 반영한다.

정답 문서가 여럿이고 그들 사이에 중요도 차이가 있을 때 가장 잘 맞는다. 대신 등급이 매겨진 정답 데이터가 필요해서 라벨링 비용이 크다.

무엇을 볼 것인가

시스템 구조가 정한다.

그리고 최소한 recall@k 하나는 반드시 재야 한다. 이것이 없으면 M10 세션 140의 3계층 진단에서 첫 두 계층을 구별할 수 없다.

확인 질문

  1. 리랭커가 있는 시스템에서 1단계 검색의 핵심 지표는 무엇이며 왜인가?
  2. MRR이 부적절한 상황을 하나 말해 보라.
  3. nDCG가 다른 지표보다 요구하는 것은 무엇인가?
확인

1. recall@k다. 1단계는 정답을 후보 집합에 넣기만 하면 되고 순서는 리랭커가 정하므로, k를 크게 잡은 recall이 중요하다.

2. 답이 여러 문서에 흩어져 있어 전부 찾아야 하는 경우다. MRR은 첫 번째 정답의 순위만 보므로 나머지를 놓쳐도 점수가 높다.

3. 관련도에 등급이 매겨진 정답 데이터다. 이진 라벨이 아니라 완전 관련·부분 관련 등의 구분이 필요해 라벨링 비용이 크다.


Session 155생성 지표

faithfulness와 groundedness가 핵심이다. "답변의 각 주장이 주어진 문서에 실제로 있는가"를 재는 것이고, 이것이 환각을 측정 가능하게 만드는 유일한 방법이다.

환각을 정의하기

"환각"은 모호한 말이다. 측정하려면 정확히 정의해야 한다.

RAG 맥락에서는 이렇게 좁힐 수 있다. 답변에 있는 주장 중 제공된 문서로 뒷받침되지 않는 것.

이 정의의 장점은 판정이 가능하다는 것이다. 세상의 모든 사실을 알 필요 없이, 주어진 문서만 보면 된다.

faithfulness 측정

절차는 이렇다.

이 지표는 문서에 근거하지 않은 내용을 만들어 내는 정도를 잰다. 답이 맞는지와는 다른 문제다. 문서가 틀렸으면 충실한 답변도 틀린 답변이다.

함께 봐야 할 지표들

faithfulness만 보면 함정이 있다. 아무 말도 하지 않으면 faithfulness가 완벽하다. "문서에 따르면 정보가 있습니다"라고만 답하면 근거 없는 주장이 하나도 없다.

그래서 함께 본다.

이 조합이 널리 쓰이는 RAG 평가 프레임워크들의 기본 지표 집합이다.

자동화

이 지표들을 사람이 매기면 비용이 크다. 그래서 LLM에게 시키는데, 그것이 다음 세션의 주제다.

다만 일부는 자동화할 수 있다.

확인 질문

  1. RAG 맥락에서 환각을 어떻게 정의하면 측정 가능해지는가?
  2. faithfulness만 보면 안 되는 이유는?
  3. 생성 평가에서 코드로 자동화할 수 있는 것을 하나 말해 보라.
확인

1. "답변의 주장 중 제공된 문서로 뒷받침되지 않는 것"으로 좁히면, 세상의 모든 사실을 알 필요 없이 주어진 문서만 보고 판정할 수 있다.

2. 아무 말도 하지 않으면 faithfulness가 완벽해지기 때문이다. answer relevance와 completeness를 함께 봐야 한다.

3. 인용한 문서 번호가 실제로 존재하는지 확인하는 것, 답변의 숫자가 문서에 있는지 매칭하는 것, 기대 답변의 핵심 단어 포함 여부를 확인하는 것이다.


Session 156LLM-as-a-Judge

사람 대신 LLM이 채점한다. 싸고 빠르고 확장 가능하지만, 판정자 자신을 먼저 평가하지 않으면 그 점수는 아무것도 보증하지 않는다.

왜 쓰는가

앞 세션의 지표들을 사람이 매기면 항목당 몇 분이 걸린다. 500건이면 며칠이다. 그리고 프롬프트를 바꿀 때마다 다시 해야 한다.

LLM이 하면 몇 분 만에 끝나고 반복이 자유롭다. 그래서 빠른 반복이 가능해진다. 이것이 가장 큰 가치다.

어떻게 쓰는가

판정자를 평가하기

이 세션의 핵심이다. judge를 도입하기 전에 judge가 믿을 만한지 확인해야 한다.

절차는 이렇다.

이 검증 없이 judge 점수를 신뢰하면, judge의 편향을 최적화하게 된다. M6 세션 80의 reward hacking과 정확히 같은 구조다.

판정 프롬프트

몇 가지가 성능을 크게 바꾼다.

확인 질문

  1. LLM-as-a-Judge의 가장 큰 가치는 무엇인가?
  2. judge를 도입하기 전에 반드시 해야 할 일은?
  3. 판정 프롬프트에서 근거를 결론보다 먼저 쓰게 하는 이유는?
확인

1. 빠른 반복이 가능해지는 것이다. 사람 채점은 며칠이 걸려 프롬프트를 바꿀 때마다 다시 할 수 없지만, judge는 몇 분이면 된다.

2. 사람이 라벨링한 소규모 집합으로 judge의 판정과 사람의 판정이 얼마나 일치하는지 측정하는 것이다. 사람끼리의 일치도가 비교 기준이다.

3. 판정 이유를 생성하며 상황을 정리하게 되어 일관성이 오른다. 결론을 먼저 내면 사후 정당화가 된다.


Session 157judge의 편향들

위치, 자기 선호, 길이. 세 편향이 반복적으로 확인되었고, 각각 측정하고 완화하는 방법이 있다.

위치 편향

pairwise 비교에서 먼저 제시된 답변을 더 자주 고른다. 어떤 모델은 뒤를 선호하기도 한다.

측정 방법이 간단하다. 같은 쌍을 순서만 바꿔 두 번 판정시킨다. 두 판정이 일치하면 순서에 영향받지 않은 것이고, 뒤집히면 위치 편향이다. 뒤집히는 비율이 그 편향의 크기다.

완화도 같은 방법이다. 양쪽 순서로 다 돌려 결과가 일치할 때만 인정한다. 불일치하면 무승부로 처리한다. 비용이 두 배지만 신뢰도가 크게 오른다.

자기 선호

judge가 자기와 같은 모델이 생성한 답변을 더 높게 평가하는 경향이다. 스타일이 익숙하기 때문으로 추정된다.

이것이 실무에서 문제가 되는 경우는 명확하다. GPT 계열로 생성하고 GPT 계열로 판정하면서 다른 모델과 비교하면, 결과가 왜곡된다.

완화는 판정 모델과 생성 모델을 다르게 하거나, 여러 judge의 판정을 종합하는 것이다.

길이 편향

측정 방법은 답변 길이와 점수의 상관계수를 보는 것이다. 상관이 강하면 편향이 있다.

완화는 두 가지다. 판정 기준에 "길이는 평가하지 말라"고 명시하거나, 비교하는 답변들의 길이를 비슷하게 맞춘다.

그 밖의 편향

종합

judge를 실무에 쓸 때의 체크리스트는 이렇다.

사람 라벨과의 일치도를 측정했는가. 위치 편향을 순서 뒤집기로 측정하고 완화했는가. 길이와 점수의 상관을 확인했는가. 판정 모델과 생성 모델이 같지 않은가. 점수 척도 대신 이진이나 pairwise를 쓰고 있는가.

이것들을 확인하지 않은 judge 점수는 참고는 되지만 근거는 되지 못한다.

확인 질문

  1. 위치 편향을 측정하는 방법과 완화하는 방법은?
  2. 자기 선호 편향이 실무에서 문제가 되는 상황을 말해 보라.
  3. 점수 척도보다 이진 판정이나 pairwise가 안정적인 이유는?
확인

1. 같은 쌍을 순서만 바꿔 두 번 판정시켜 뒤집히는 비율을 본다. 완화는 양쪽 순서로 다 돌려 일치할 때만 인정하고 불일치는 무승부로 처리하는 것이다.

2. 같은 계열 모델로 생성하고 판정하면서 다른 계열 모델과 비교할 때다. 자기 계열이 부당하게 높게 평가된다.

3. 여러 단계의 점수를 일관되게 매기기 어려워 중간 점수를 잘 안 주고 극단으로 몰리기 때문이다. 이진 판정이나 두 답변 비교는 판단이 단순해 일관성이 높다.


Session 158표본 크기와 신뢰구간

100건에서 72퍼센트가 75퍼센트가 된 것은 대개 아무 의미가 없다. 표본에서 관측한 차이가 진짜인지 노이즈인지 구별하는 것이 이 세션의 주제다.

문제

평가셋 100건에서 A 버전이 72점, B 버전이 75점이 나왔다. B가 나은가.

직관은 그렇다고 말하지만 틀렸을 수 있다. 100건은 작은 표본이고, 같은 시스템을 두 번 평가해도 몇 퍼센트 차이가 나는 것이 정상이다.

신뢰구간

관측된 값 주위에 참값이 있을 만한 범위를 계산한다.

비율에 대한 대략적인 계산은 이렇다. 관측 비율이 p, 표본 크기가 n일 때 95퍼센트 신뢰구간의 폭은 대략 이렇다.

±1.96p(1p)n

p=0.72, n=100이면 ±0.088, 즉 63퍼센트에서 81퍼센트다. B의 75퍼센트도 66에서 84퍼센트다. 두 구간이 크게 겹친다. 3퍼센트 차이는 이 정도 표본에서 구별되지 않는다.

필요한 표본 크기

거꾸로 계산할 수 있다. 5퍼센트 차이를 구별하려면 표본이 얼마나 필요한가.

위 식에서 폭이 2.5퍼센트가 되려면 n이 대략 1500이다. 10퍼센트 차이를 구별하려면 n이 약 380이면 된다.

표본이 작으면 차이가 보이지 않는다

100
3%p
가로선이 95% 신뢰구간이다. 두 구간이 겹치면 그 차이를 주장할 수 없다. 표본을 키우거나 차이가 커져야 구간이 분리된다. 손잡이를 움직여 "이 정도 차이를 잡으려면 몇 건이 필요한가"를 직접 확인해 보라.

쌍체 비교

다행히 더 나은 방법이 있다. 두 버전을 같은 항목에 대해 평가하므로, 독립적인 두 표본이 아니라 쌍을 이룬 데이터다.

각 항목에서 A와 B의 결과를 비교해 "B가 이김", "무승부", "A가 이김"으로 분류한다. 그러면 둘 다 맞히거나 둘 다 틀린 항목이 상쇄되고, 차이가 난 항목만 신호가 된다.

이 방식은 훨씬 적은 표본으로 같은 결론에 도달한다. 두 버전이 대부분의 항목에서 같은 결과를 내고 몇 개에서만 다르다면, 그 몇 개가 일관되게 한쪽으로 기울었는지만 보면 되기 때문이다.

부트스트랩

신뢰구간을 계산하는 실용적인 방법이 있다. 수식을 유도하지 않고 재표본추출로 구한다.

평가 결과에서 무작위로 복원 추출해 같은 크기의 가상 표본을 만들고 점수를 계산한다. 이것을 천 번 반복하면 점수의 분포가 나온다. 그 분포의 2.5퍼센타일과 97.5퍼센타일이 95퍼센트 신뢰구간이다.

실무 규칙

정리하면 이렇다.

확인 질문

  1. 100건에서 3퍼센트 차이가 의미 없을 수 있는 이유를 계산으로 설명해 보라.
  2. 쌍체 비교가 독립 비교보다 적은 표본으로 결론을 낼 수 있는 이유는?
  3. 부트스트랩의 장점은 무엇인가?
확인

1. p=0.72,n=100이면 95% 신뢰구간이 약 ±8.8%p라 63~81%다. 3%p 차이는 이 구간 안에 완전히 묻힌다.

2. 같은 항목에 대해 비교하므로 둘 다 맞히거나 둘 다 틀린 항목이 상쇄되고, 결과가 갈린 항목만 신호가 된다. 노이즈가 줄어든다.

3. 수식 유도 없이 어떤 지표에도 적용할 수 있다는 것이다. 재표본추출을 반복해 분포를 얻으면 되므로 복잡한 조합 지표에도 쓸 수 있다.


Session 159회귀 탐지

개선은 의도한 것이지만 회귀는 의도하지 않은 것이다. 프롬프트 한 줄을 고쳤다가 다른 유형이 무너지는 일이 흔하고, 그것을 자동으로 잡는 장치가 필요하다.

왜 생기는가

프롬프트는 전역 설정이다. 한 유형의 질문을 개선하려고 지시를 추가하면, 다른 유형의 질문에도 그 지시가 적용된다.

예를 들어 "간결하게 답하라"를 넣으면 장황한 답변이 개선되지만, 상세한 설명이 필요한 질문에서 정보가 빠진다. 평균 점수는 오르는데 특정 유형이 무너진다.

모델을 바꿀 때, 인덱스를 재구축할 때, 청킹을 바꿀 때도 같은 일이 일어난다.

유형별로 본다

전체 점수만 보면 이 현상을 놓친다. 그래서 평가셋에 유형 라벨을 붙이고 유형별 점수를 함께 본다.

M12 세션 152에서 메타데이터를 넣으라고 한 이유가 이것이다. 질문 유형, 난이도, 도메인별로 나눠 보면 어디가 무너졌는지 보인다.

리포트는 이런 형태가 된다. 전체 78 → 81, 단순 조회 85 → 92, 다중 문서 종합 70 → 71, 부정 조건 68 → 52. 마지막 줄이 회귀다. 전체 점수만 봤으면 발견하지 못했다.

항목 단위로 본다

더 정밀한 방법은 개별 항목의 변화를 보는 것이다.

세션 158의 쌍체 비교와 같은 발상이다. 각 항목에서 A와 B의 결과를 비교해 개선된 것, 나빠진 것, 같은 것으로 분류한다.

그러면 이런 리포트가 나온다. 500건 중 개선 62건, 회귀 24건, 변화 없음 414건.

CI에 붙이기

수동으로 하면 하지 않게 된다. 자동화한다.

프로덕션 모니터링

오프라인 평가만으로는 부족하다. 실제 트래픽의 분포가 변하기 때문이다.

프로덕션에서 이상이 감지되면 그 사례를 평가셋에 추가한다. 평가셋이 시간에 따라 자란다.

확인 질문

  1. 전체 점수만 보면 회귀를 놓치는 이유를 예로 설명해 보라.
  2. 항목 단위 비교가 유형별 집계보다 나은 점은?
  3. 평가 비용을 관리하는 2단계 방식은?
확인

1. 어떤 유형은 크게 개선되고 다른 유형은 크게 나빠져도 평균은 오를 수 있다. 예컨대 "간결하게 답하라"를 넣으면 장황한 답변은 개선되지만 상세 설명이 필요한 질문이 무너진다.

2. 유형 집계로도 안 잡히는 미묘한 패턴이 개별 회귀 사례를 직접 읽으면 보인다. 원인 특정이 훨씬 쉽다.

3. 소규모 셋(100건)을 매 변경마다 돌려 빠르게 확인하고, 전체 셋은 병합 전에 한 번 돌린다.


Session 160관측성 설계

트레이스에 토큰과 비용이 붙어야 한다. 지연만 보는 관측은 LLM 시스템에서 절반만 보는 것이다.

무엇을 남기는가

요청 하나의 전체 경로를 재구성할 수 있어야 한다. RAG 에이전트라면 이런 span들이 나온다.

요청 접수 → 질의 재작성 → 검색(BM25, 벡터) → 융합 → 리랭킹 → 프롬프트 구성 → 모델 호출 → 도구 실행 → 모델 호출 → 응답.

각 span에 무엇을 붙일지가 핵심이다.

프롬프트를 남길 것인가

민감한 질문이다.

절충안이 몇 가지 있다.

정답은 도메인에 따라 다르다. 다만 아무것도 남기지 않으면 디버깅이 불가능하다는 점은 분명하다.

비용 대시보드

토큰과 비용을 span에 넣어 두면 여러 축으로 집계할 수 있다.

이 데이터가 없으면 최적화할 곳을 찾을 수 없다. 비용은 측정하지 않으면 반드시 새어 나간다.

표준

OpenTelemetry에 생성형 AI를 위한 규약이 정의되어 있다. span 이름과 속성 이름을 표준화한 것이다.

표준을 따르면 관측 도구를 바꿔도 계측 코드를 다시 쓰지 않아도 되고, 여러 시스템의 데이터를 함께 볼 수 있다.

확인 질문

  1. 모델 호출 span에 반드시 넣어야 할 속성 세 가지를 말해 보라.
  2. 프롬프트 전문을 로그에 남길 때의 딜레마와 절충안은?
  3. 비용을 여러 축으로 집계하는 것이 왜 필요한가?
확인

1. 입력·출력 토큰 수, 비용, TTFT와 총 지연이다. 모델 이름, 캐시 적중 여부, 종료 이유도 중요하다.

2. 디버깅에는 필수인데 개인정보와 기밀이 쌓인다. 절충안은 해시만 남기기, 샘플링, 마스킹, 짧은 보관 기간, 접근 권한 분리다.

3. 어떤 기능·사용자·단계가 비용을 지배하는지 알아야 최적화할 곳을 찾을 수 있기 때문이다. 측정하지 않으면 비용은 반드시 새어 나간다.


Session 161캐싱 계층과 그 위험

세 계층이 있고 효과와 위험이 정반대다. prefix cache는 안전하고, result cache는 조건부로 안전하며, semantic cache는 가장 위험하다.

세 계층

RAG에서 특히 중요하다. 문서가 갱신됐는데 캐시된 옛 답변이 나가면 잘못된 정보를 제공하는 것이다.

semantic cache가 위험한 이유

효과는 크다. 같은 취지의 질문이 다양한 표현으로 오는 서비스에서 적중률이 높다.

그런데 비슷하지만 다른 질문에 잘못된 답을 준다.

"연차는 며칠인가"와 "병가는 며칠인가"는 임베딩상 매우 가깝다. 임계값을 조금만 낮게 잡으면 병가 질문에 연차 답이 나간다.

"2023년 매출"과 "2024년 매출"도 가깝다. 숫자가 하나 다를 뿐이다. M10 세션 134에서 본 대로 임베딩은 숫자와 고유명사의 정확한 차이를 잘 다루지 못한다.

그리고 이 실패는 조용하다. 에러가 나지 않고 그럴듯한 답이 나간다. 사용자가 알아채지 못할 수도 있다.

안전하게 쓰려면

그래도 쓰고 싶다면 여러 장치를 건다.

순서

정리하면 도입 순서는 이렇다. prefix cache를 먼저 켠다. 위험이 없다. result cache를 다음에, 무효화 전략과 함께 설계한다. semantic cache는 마지막에, 위험을 감수할 가치가 있는지 판단한 뒤.

확인 질문

  1. 세 캐시 계층을 위험도 순으로 나열하고 이유를 말해 보라.
  2. RAG에서 result cache를 쓸 때 반드시 처리해야 할 것은?
  3. semantic cache의 실패가 특히 위험한 이유는?
확인

1. prefix cache는 계산 결과 재사용이라 출력이 달라지지 않아 안전하다. result cache는 키 설계와 무효화만 제대로 하면 안전하다. semantic cache는 비슷하지만 다른 질문에 잘못된 답을 줄 수 있어 가장 위험하다.

2. 참조하는 문서가 갱신되면 캐시를 무효화하는 것이다. 그리고 캐시 키에 사용자 권한 등 모든 입력이 포함되어야 한다.

3. 에러 없이 그럴듯한 답이 나가 조용히 실패하기 때문이다. 임베딩이 숫자나 고유명사의 차이를 잘 구별하지 못해 "2023년"과 "2024년" 같은 질문이 혼동될 수 있다.


Session 162비용·지연 최적화의 순서

컨텍스트 → 캐싱 → 라우팅 → 양자화·배치. 품질 저하 위험이 낮은 것부터 간다. 그리고 각 단계마다 평가셋으로 확인한다.

왜 순서가 있는가

최적화 수단마다 비용 절감 효과품질 위험이 다르다. 위험이 낮으면서 효과가 큰 것부터 하는 것이 합리적이다.

그리고 앞 단계를 하면 뒤 단계의 필요가 줄어드는 경우가 많다. 컨텍스트를 절반으로 줄이면 그것만으로 비용이 절반이 되어 양자화까지 안 가도 될 수 있다.

1단계 — 컨텍스트 줄이기

가장 먼저, 가장 큰 효과가 나는 경우가 많다.

2단계 — 캐싱

앞 세션의 순서대로 prefix, result, semantic을 검토한다.

특히 prefix cache는 이미 켜져 있는지 확인한다. 엔진 설정에서 꺼져 있는 경우가 있고, 프롬프트 구성이 프리픽스를 깨고 있는 경우도 있다.

3단계 — 라우팅

모든 요청에 가장 큰 모델을 쓸 필요는 없다.

4단계 — 양자화와 배치

M8과 M9의 내용이다. 인프라 계층의 최적화다.

이 단계는 효과가 크지만 품질 위험과 운영 복잡도가 따라온다. 앞 단계들을 다 해 본 뒤에 온다.

각 단계마다 확인한다

이것이 이 모듈의 결론이기도 하다. 모든 최적화 후에 평가셋을 돌린다.

비용이 30퍼센트 줄었는데 품질이 5퍼센트 떨어졌다면, 그 교환이 받아들일 만한지 판단해야 한다. 그리고 그 판단을 하려면 숫자가 있어야 한다.

측정 없이 최적화하면 비용은 확실히 줄고 품질은 모르게 떨어진다. 그것이 이 분야에서 가장 흔한 사고다.

확인 질문

  1. 최적화 순서를 나열하고 그 순서인 이유를 말해 보라.
  2. 컨텍스트를 줄이는 것이 오히려 품질을 올릴 수도 있는 이유는?
  3. 모델 라우팅의 대가는 무엇인가?
확인

1. 컨텍스트 줄이기 → 캐싱 → 라우팅 → 양자화·배치. 품질 저하 위험이 낮고 효과가 큰 것부터 가며, 앞 단계를 하면 뒤 단계의 필요가 줄어든다.

2. 관련 없는 문서가 줄어 신호 대 잡음비가 좋아지고, 정답이 중간에 묻히는 현상도 완화되기 때문이다.

3. 복잡성이다. 두 모델의 동작이 달라 평가가 두 배가 되고, 라우팅 오류라는 새로운 실패 유형이 생긴다.


Session 163복습 — 시스템 하나를 처음부터 끝까지

관문 G4이자 이 트랙의 마지막 세션이다. 요구사항을 주고 시스템 전체를 말로 설계하며, 각 선택의 트레이드오프를 댈 수 있어야 한다.

이 모듈 이어 보기

LLM 시스템의 품질은 자명하지 않아 먼저 무엇을 잴지 정의해야 한다. 실패 사례를 모아 유형을 분류하고 가장 흔한 것부터 측정 가능하게 만든다.

평가셋은 실제 트래픽에서 온다. 질문, 정답 근거 문서 ID, 기대 답변 요지, 메타데이터를 담고, 답이 없는 질문을 반드시 포함한다. 개발용과 최종 검증용을 분리한다.

채점은 LLM에게 시킬 수 있지만 judge를 먼저 검증해야 한다. 사람 라벨과의 일치도를 재고, 위치·자기 선호·길이 편향을 측정하고 완화한다.

모든 결과에 신뢰구간을 붙인다. 100건에서 3퍼센트 차이는 노이즈다. 두 버전 비교는 쌍체로 하고, 구간이 겹치면 차이 없음으로 보고한다.

관측은 토큰과 비용을 span에 담아야 하고, 캐싱은 prefix → result → semantic 순으로, 최적화는 컨텍스트 → 캐싱 → 라우팅 → 인프라 순으로 간다.

관문 G4 — 설계 문제

최종 자가 점검

  1. "검색이 안 됩니다"의 원인을 세 계층으로 나누고 각각의 확인 방법을 말해 보라. (M10)
  2. "응답이 느립니다"에 대해 원인 후보를 순서대로 나열해 보라. (M7, M9)
  3. 8B 모델·80GB GPU·컨텍스트 16k에서 동시 요청 수를 계산해 보라. (M8)
  4. "사내 규정을 모른다"에 RAG를 고르는 이유 두 가지는? (M6)
  5. attention에서 d로 나누는 이유를 세 단계로 설명해 보라. (M4)
  6. KV cache가 저장하는 것과 그 크기 공식은? (M7)
  7. continuous batching이 처리량을 올리는 메커니즘은? (M9)
  8. DPO가 보상 모델을 소거할 수 있는 근거는? (M6)
  9. judge를 신뢰하기 전에 측정해야 할 것 세 가지는? (M12)
  10. 결정론적 코드와 확률적 모델의 경계를 긋는 기준은? (M11)
확인

1. 정답이 인덱스에 있는가(문자열 검색), 검색이 찾는가(brute-force와 비교해 ANN·임베딩·리랭커 중 특정), 모델이 쓰는가(컨텍스트에 들어갔는데 답이 틀린 경우).

2. 먼저 TTFT인지 TPOT인지 나눈다. TTFT면 프롬프트 길이, 큐 대기, 프리픽스 캐시 미스, 토큰화. TPOT면 배치 크기, 모델 크기, KV cache 크기, 컨텍스트 길이. 총 시간이면 응답 길이와 한국어 토큰 효율.

3. 가중치 16GB와 여유 8GB를 빼면 56GB. 요청당 16384×128KiB=2GiB이므로 약 28개다.

4. 규정이 바뀌어도 문서만 갱신하면 되고, 어느 문서에서 온 답인지 근거를 제시할 수 있다.

5. 분산 1인 항 d개를 더하므로 내적의 표준편차가 d가 된다. 그 진폭이 softmax를 포화시킨다. 포화된 지점에서 기울기가 0에 가까워 학습이 멈춘다.

6. 각 층의 K와 V를 저장한다. Q는 현재 토큰 것만 필요해 저장하지 않는다. 크기는 2×L×Hkv×dh×T×B×bytes이고 8B급에서 토큰당 128 KiB다.

7. 배치 단위를 요청에서 디코딩 스텝으로 낮춰 끝난 요청을 빼고 대기 요청을 즉시 넣는다. 슬롯이 노는 시간이 사라져 같은 가중치 읽기로 더 많은 토큰을 처리한다.

8. KL 제약이 붙은 보상 최대화의 최적 정책이 닫힌 형태로 존재하고, 그것을 뒤집으면 보상을 정책으로 표현할 수 있다. 그 표현을 보상 모델 손실에 대입하면 보상이 소거된다.

9. 사람 라벨과의 일치도, 순서를 뒤집었을 때의 판정 반전율(위치 편향), 답변 길이와 점수의 상관(길이 편향)이다.

10. 규칙으로 쓸 수 있는 것은 전부 코드로 강제하고, 판단이 필요한 것만 모델에 맡기되 그 출력도 결정론적 검증을 통과시킨다.

트랙을 마치며

163개의 세션을 지나왔다. 되돌아보면 하나의 사슬이었다.

모델이 학습하는 것은 다음 토큰의 확률분포 하나뿐이고(M0), 그것을 이해하려면 softmax와 cross-entropy라는 어휘가 필요했다(M1). 그 어휘로 신경망의 학습 한 스텝을 따라갔고(M2), 문장이 모델에 들어가는 경로를 봤다(M3). 그리고 attention이 무엇을 계산하는지를 텐서 모양 단위로 추적했다(M4).

그 모델이 어떻게 만들어지고(M5, M6) 어떻게 돌아가는지(M7), 왜 느린지(M8), 엔진이 그것을 어떻게 푸는지(M9)를 봤다. 마지막으로 그 모델로 시스템을 만들고(M10, M11) 그것이 잘 동작하는지 아는 방법(M12)까지 왔다.

그리고 하나 덧붙이면, 이 분야는 6개월이면 세부가 바뀐다. 여기서 배운 도구 이름들은 몇 년 안에 다른 것으로 대체될 것이다. 그러나 왜 decode가 대역폭에 묶여 있는지, 왜 검색과 생성을 분리해 평가해야 하는지, 왜 규칙으로 쓸 수 있는 것을 코드로 강제해야 하는지는 바뀌지 않는다. 이 트랙이 걸어 둔 것은 그쪽이다.

M12 끝

확률분포에서 프로덕션까지 · 163세션 합본 · 2026-09-01
관문은 세션 21 · 62 · 114 · 163이다. 진도 계획과 자료 목록은 로드맵에 있다.