로딩중…

AI Engineering

개발개발 ·

양자화란 무엇인가

4개월 만에 블로그 포스팅을 재개하게 되었네요. 이런저런 난관도 있었고, 새 직장에 안착해 자리를 잡아야 했고, 새 프로젝트에 투입되어 AIOps 개발을 하느라 포스팅이 늦어졌습니다. 이번 회사에서 제 커리어를 웹 백엔드에서 AIOps로 바꾸게 되었는데, 아직 새로운 것도 많고 공부할 것도 많아 바쁜 하루하루를 보내고 있습니다.

이직 이야기나 직종을 어떻게 바꾸게 되었는지는 회고에서 더 자세히 풀기로 하고, 지금은 AIOps 개발에 필요한 이론 공부를 중점적으로 하고 있습니다.

이번 포스팅에서는 LLM으로 개발하는 사람이라면 모두 들어봤을 양자화를 공부해 보고, 첫 포스팅인 만큼 용어 정리와 파인튜닝에 대한 간단한 개요까지 적어 볼까 합니다.

부동소수점 아래 숫자 표현이란?

LLM 모델을 이야기할 때 흔히 fp32, fp16 같은 표현을 씁니다. 양자화를 알아보기 전에 이 개념부터 짚고 넘어가야 합니다.

fp32와 fp16은 부동소수점 아래의 숫자를 어떻게 표현할 것인가에 대한 것입니다. fp32는 32bit(4byte)로, fp16은 16bit(2byte)로 표현하겠다는 뜻이죠. 개인 컴퓨터에서 많이 쓰는 "4bit 양자화"는 같은 방식으로 표현하면 fp4가 됩니다.

여기서 드는 의문은 "부동소수점이 왜 갑자기 나오느냐"일 텐데요. 이 부동소수점은 간단히 말해 "확률"을 표현한 것입니다.

Transformer 기반 LLM은 어떤 단어 뒤에 올 다음 단어를 "확률적으로" 계산해 생성한다고 알려져 있습니다. 이때 쓰는 확률이 바로 부동소수점으로 표현됩니다.

예를 들어 "사과는 (?)" 뒤에 올 단어를 고른다면 이렇습니다.

• 맛있다 → 0.76543210

• 빨갛다 → 0.61234567

• 자동차 → 0.01234567

이렇게 다음 단어가 올 확률을 부동소수점 아래로 표현하고, 가장 높은 확률을 기록한 것을 다음 단어로 씁니다. 결과적으로 "사과는 맛있다"라는 문장이 완성되는 것이죠.

물론 실제 LLM은 단어 하나만 보는 게 아니라 문장 전체를 보고 계산하기 때문에 이렇게 단순하지 않습니다. 직관적인 예시를 위해 많은 부분을 생략했습니다.

fp32는 부동소수점 아래 7~9자리, fp16은 4~5자리, fp4는 1~2자리까지 표현합니다. 그래서 위의 예시는 이렇게 됩니다.

맛있다

• fp32 : 0.76543210

• fp16 : 0.76543

• fp4 : 0.76

빨갛다

• fp32 : 0.61234567

• fp16 : 0.61234

• fp4 : 0.61

보시다시피 양자화를 하더라도 단어의 순위 자체는 잘 바뀌지 않습니다. 그래서 성능이 어느 정도 방어되는 것이죠.

이런 이유로 "작은 모델을 쓰는 것보다 큰 모델을 양자화해서 쓰는 것이 성능에 더 도움이 된다"는 이야기가 정설로 받아들여집니다.

베이스 모델(fp32)에 가까울수록 다음 단어를 고르는 것이 더 정교해지고, 그래서 성능이 좋아지는 것이죠.

다음으로 넘어가려면 LLM의 직접적인 성능으로 표현되는 "파라미터"를 짚어야 합니다.

파라미터란?

파라미터는 모델이 세상을 얼마나 다채롭게 보는가와 관련된 지표입니다. LLM이 얼마나 깊이 있게 생각할 수 있는지에 대한 척도라고 보시면 됩니다.

예를 들어 파라미터가 작은 모델은 "키가 크면 몸무게가 많이 나간다"처럼 단순하게 사고합니다. 반면 큰 모델은 "키가 크더라도 마른 체형이면 몸무게가 적을 수 있고, 근육질이라면 더 많이 나갈 수 있다"처럼 복잡한 현실을 더 정교하게 묘사합니다.

파라미터는 흔히 7B, 20B 같은 숫자로 나타내며, 각각 70억 개·200억 개의 파라미터를 학습시켰다는 뜻입니다.

쉽게 말하면 파라미터는 모델이 얼마나 많은 차원(매개변수)을 가지고 있는가에 대한 척도입니다. 보통 `y = ax + b`를 1차 함수, 즉 매개변수가 하나인 함수라고 하죠.

그 매개변수가 70억 개 있다는 이야기인데, 각 차원은 세상에 존재하는 것들에 대한 기준을 나타냅니다. 첫 번째 매개변수는 성별, 두 번째는 색깔, 세 번째는 지위, 네 번째는 크기… 이런 식으로요.

70억 개라면 세상을 보는 기준이 70억 개, 200억 개라면 200억 개라는 이야기입니다. 그러니 큰 모델일수록 세상을 다채롭게 보는 것이죠.

양자화란?

양자화는 fp32의 베이스 모델을 fp16, fp4로 대충 뭉뚱그려 표현하는 것입니다. 앞서 부동소수점을 이야기하며 대강 다룬 내용이죠.

다만 양자화 기법은 "이 소수점을 어떻게 대충 표현할 것인가"를 정하는 방법이라, 종류가 다양합니다. 이번 포스팅에서는 그중 가장 대표적인 세 가지를 소개하겠습니다.

GPTQ

현재 GPU 환경에서 널리 쓰이는 범용 양자화 기법입니다. 베이스 모델인 fp32를 fp16이나 fp4로 깎아낼 때 단순히 반올림하는 것이 아니라 두 번 미분한 값을 가지고 양자화하기 때문에 오차가 매우 적습니다.

그래서 단순히 자르는 방식보다 성능 보존율이 높아 저점 방어가 확실한 편입니다. 생태계도 성숙해서 지원 라이브러리가 많고, 추론 속도도 매우 빨라집니다.

다만 양자화 변환 과정이 다소 느리고 무겁다는 단점, 그리고 주로 가중치만 양자화하고 활성화 값은 다루지 않는다는 단점이 있습니다.

#### cf) 여기서 잠깐! 가중치와 활성화 값이란?

가중치는 모델이 학습을 통해 배운 지식 그 자체입니다. 학습이 끝나면 이 값은 고정되고, 값이 클수록 해당 정보가 중요하다는 뜻입니다.

맞습니다. 방금 이야기한 파라미터와 같은 말입니다. 가중치 == 파라미터라고 이해해도 전혀 무방합니다.

활성화 값은 입력값 × 가중치를 계산한 뒤 "활성화 함수"를 거쳐 나온 결괏값입니다. 이 값이 특정 기준을 넘어야 다음 단계로 정보가 전달됩니다. 활성화 함수는 작년에 노벨물리학상을 받은 제프리 힌튼 교수가 1986년에 쓴 논문에서 나온 근본 개념입니다. 힌튼 교수는 이 논문을 기점으로 활성화 함수, 역전파 등의 개념을 만든 장본인이죠.

활성함수
활성함수

AWQ

GPTQ의 강력한 경쟁자로, "모든 가중치가 똑같이 중요하지는 않다"는 점에 착안한 기술입니다. AWQ는 GPTQ가 손대지 않는 활성화 값을 분석해, 중요한 1%의 가중치는 그대로 두고 덜 중요한 가중치만 4bit로 과감하게 압축합니다. 이렇게 하면 전체적인 성능 하락을 막을 수 있습니다.

또한 GPTQ에 비해 과적합 문제가 생기지 않고, 다양한 작업에서 조금 더 나은 성능을 보여 주는 경우가 많습니다. 고속 추론 엔진인 vLLM이 밀고 있는 기법이라는 점에서 떠오르는 초신성 같은 기술이죠.

다만 변환 과정이 GPTQ보다 조금 더 복잡하고 지원 도구도 GPTQ만큼 방대하지는 않습니다. 그래도 vLLM이 밀고 있어서 생태계가 빠르게 발전하고 있습니다.

과적합이 생기지 않는 이유는 이렇게 짐작해 봅니다. GPTQ는 가중치를 세밀하게 조정하다 보니 값이 한곳에 몰릴 가능성이 있는 반면, AWQ는 중요하지 않은 가중치를 과감하게 잘라 내기 때문에 그런 쏠림이 덜 생기는 것이 아닐까 싶습니다.

GGUF

CPU에서 LLM을 돌려야 하거나 애플 실리콘에서 돌릴 때 사실상의 표준 포맷입니다. 엄밀히 말하면 양자화 알고리즘이 아니라 파일 포맷에 가깝고, 주로 llama.cpp의 양자화 방식을 포함합니다.

다른 양자화 기법들은 보통 NVIDIA 그래픽카드에 최적화되어 있거나, 모든 LLM 리소스가 VRAM에 정상적으로 올라와야(공유 메모리를 쓰지 않아야) 최대 성능을 내도록 설계돼 있습니다. 그래서 VRAM이 부족하거나 NVIDIA 그래픽카드가 아닌 환경에서 GPTQ나 AWQ를 쓰면 OOM이 발생할 가능성이 매우 높습니다.

반면 GGUF는 CPU에서도 돌아가도록 설계돼 있어서, VRAM이 부족하면 CPU 메모리인 DRAM을 써서 조금 느리더라도 동작은 하게 해 줍니다.

또한 모델이 단일 파일로 저장되어 가중치를 한 파일로 관리하기 편하고, mmap을 사용해 모델을 메모리에 올리는 속도가 매우 빠릅니다.

마치며

이번 포스팅에서는 파라미터와 가중치, 활성화 함수, 양자화를 소개해 드렸습니다. 앞으로 당분간은 AIOps에 대한 내용을 주로 다룰 예정이고, 이론과 더불어 실전에서 AIOps를 어떻게 활용해 레이턴시와 처리량을 개선할지 백엔드 관점까지 녹여 보겠습니다.

이번 포스팅의 결론은 이렇습니다. 순수 NVIDIA GPU 서버 환경이라면 GPTQ나 AWQ를, CPU나 애플 실리콘에서 돌려야 한다면 GGUF를 쓰면 됩니다.

사내 새 프로젝트를 진행하며 배운 여러 개념을 실전에 녹여 본 이야기가 기다리고 있으니 많은 기대 부탁드립니다.

거의 4개월 만에 다시 포스팅에 시동을 걸었습니다. 본격적으로 공부하면서 열심히 써 보겠습니다.

긴 글 읽어 주셔서 감사합니다. 오늘도 즐거운 하루 되세요!

vLLM은 어떻게 AI 엔지니어링에서 필수불가결한 선택지가 되었을까?

안녕하세요! 요즘 일이 좀 널널해져서, 마지막으로 글 쓴 지 한 달 만에 다시 개인 컴퓨터 앞에 앉았습니다. 오늘은 일하면서 쓸 일은 없지만 관심이 생겨 공부한 vLLM 이야기를 정리해 보려 합니다.

KV Cache란?

vLLM 얘기로 시작해 놓고 KV Cache가 왜 나오나 싶으시겠지만, 이 둘은 큰 연관이 있어서 서론으로 넣었습니다.

KV Cache의 본질로 들어가면 Attention 연산이 나옵니다. 저도 깊이 있게 이해한 건 아니지만(무슨 수식이 한가득…) Attention 연산에는 세 개의 매개변수가 필요합니다.

• Q (Query) : 지금 내가 찾고자 하는 정보 (질문)

• K (Key) : 지금 내가 가지고 있는 정보 (인덱스)

• V (Value) : 지금 내가 가지고 있는 실제 값 (내용)

모양새가 데이터베이스 쿼리와 비슷하죠. 이렇게 `Attention(Q, K, V)`로 행렬 곱을 진행하는데, Q와 K를 곱해 유사도(Attention Score)를 구하고 그 비중에 따라 V를 가중 합산해 최종 결과를 냅니다.

여기까지는 대충 이해하고, 깊은 공부는 나중으로 미뤄 뒀습니다.

지금까지가 데이터 관점에서 본 KV Cache라면, 백엔드 개발자 관점에서 보는 KV Cache는 이렇습니다.

"질문을 할 때마다 메모리를 미친놈처럼 많이 잡아먹고 있다… 이게 뭐지?"

사실 이 KV Cache는 Transformer 알고리즘에서 아주 중요하게 쓰이는 영역입니다. "나는 오늘 점심을 먹는다"라는 문장을 생성하려면 Transformer는 다음 단계를 거칩니다.

• 나(Q) → 는(Next Token)

• 나는(Q) → 오늘(Next Token)

• 나는 오늘(Q) → 점심(Next Token)

• 나는 오늘 점심(Q) → 을(Next Token)

• 나는 오늘 점심을(Q) → 먹는다(Next Token)

계속 반복해서 등장하는 놈들이 보이시죠? 이런 정적인 데이터의 반복은 우리를 캐싱으로 이끕니다. 웹 개발에서도 정적인 데이터는 캐싱을 도입해 응답 속도를 높이는 것처럼, KV Cache도 이름에 걸맞게 캐시 역할을 톡톡히 하고 있는 것이죠.

하지만 캐싱이 마냥 좋기만 한 것은 아닙니다. 웹 개발에서는 캐시 히트율을 높이는 것이 중점 관심사이지만, 결국 캐시를 도입하면서 생기는 운영 리스크와 비용 리스크를 함께 지게 되니까요.

결국 이것도 개발자가 늘 겪는 Trade-Off 문제입니다. 연산 속도와 메모리라는 두 가지 요건 중 하나는 무조건 포기해야 하는 상황이죠.

그럼에도 AI 엔지니어들은 KV Cache를 적극적으로 도입했습니다. 고객은 느린 걸 참지 못하거든요. 지금의 Chat AI들은 속도가 어마어마하게 빠르지만, 초기 AI들은 너무 느려서 써먹기 힘든 경우도 있었으니까요. (GPT-4 시절이 생각나네요…)

KV Cache가 메모리를 너무 많이 차지하는데?

그런데 요즘은 도가 좀 지나친 것 같습니다. 모델보다 KV Cache가 더 문제인 경우도 생기거든요.

성능은 그리 높지 않아도 입력 토큰을 많이 필요로 하는 서비스는 적게는 30만 개, 많게는 50만~100만 개까지 씁니다. 모델 크기가 7B 수준이면 8비트 양자화를 거쳐 8기가 정도인데, 토큰 30만 개가 벌써 15기가를 먹어 치웁니다.

30만 개가 이 정도인데 100만 개를 처리하려면 거의 50기가에 가까운, KV Cache만을 위한 메모리가 필요해집니다.

조금 더 큰 예를 들어 보죠. 70B짜리 LLM이 있다고 가정합시다. 흔히 쓰는 FP16으로 계산하면 모델 크기는 140GB입니다. 요즘 LLM은 Input 토큰도 엄청나게 많이 넣을 수 있죠? 100만 토큰이라고 가정해 봅시다.

한 사람이 100만 토큰을 넣으면 이를 메모리로 환산했을 때 약 200GB 정도 됩니다.

"오잉? 아까는 50기가라면서요?"

그건 7B일 때 이야기입니다. KV Cache의 크기는 컨텍스트 길이와 배치 사이즈에도 비례하기 때문에, 작은 모델과 큰 모델은 차이가 큽니다.

아무튼 모델 크기는 140GB인데 KV Cache가 200GB라니. 만약 100명의 사용자가 붙으면? 2TB가 필요하네요.

이게 DRAM도 아니고 HDD 저장 공간도 아닙니다. VRAM이라는 게 중요한 거죠. 요즘 나오는 H200이 80GB의 VRAM을 제공하니, 2TB가 필요하다면 H200이 250대나 있어야 합니다.

그런데 GPT 활성 이용자 수가 수억 명씩 되니 실제 동시 접속자는 수백만~수천만 명일 겁니다. AI 거대 기업들이 데이터센터를 미친 듯이 늘리는 이유가 여기 있겠죠.

하지만, 이건 틀렸습니다

어마어마한 수치이긴 하지만, 이런 문제를 눈 뜨고 지켜볼 개발자들이 아니었습니다. LLM 모델에 양자화를 적용했던 것처럼 KV Cache에도 양자화를 적용하기로 한 것이죠. 그리고 이 아이디어는 문제의 심각성을 깨닫고 얼마 지나지 않아 바로 개발이 완료됐습니다.

양자화에 대한 내용은 아래 링크에 자세히 설명해 뒀습니다. 한번 보고 오시길 추천드립니다.

관련 글: https://coding-review.tistory.com/607

KV Cache를 양자화하니 거대 모델을 양자화했을 때와 똑같은 장단점이 생기기 시작했습니다. 크기 자체는 줄일 수 있었지만 여러 정합성 문제와 성능 문제가 대두된 것이죠.

4비트 양자화를 하면 기존 FP16 대비 크기를 4배 줄일 수 있었습니다. 그런데 또 다른 문제가 생기기 시작했습니다.

메모리 단편화 문제

OS를 공부하며 가상 메모리 개념을 보셨거나, JVM 기반 개발을 하면서 GC의 Mark-Sweep-Compact 알고리즘을 공부하셨다면 들어 보셨을 "메모리 단편화 문제"가 새롭게 떠올랐습니다.

메모리 단편화란 쉽게 말해, 메모리를 점유하던 프로세스가 종료되면서 그 자리가 비어 구멍이 뚫리는 상황을 뜻합니다.

가상 메모리(Paging)는 이 문제를 해결하기 위해 프로세스에는 논리 주소를 부여하고, 실제 물리 주소는 MMU(Memory Management Unit)에 위임해 논리 주소와 물리 주소를 재빠르게 바꿔치기할 수 있도록 만들어졌습니다.

쉽게 풀어서 설명해 보겠습니다.

□ □ □ □ □ □

메모리에 이렇게 여섯 개의 공간이 있습니다. 이때 A 프로세스와 B 프로세스가 각각 2와 1만큼의 공간을 달라고 요청합니다. MMU는 A와 B에게 논리 주소를 부여합니다.

1 2 3 4 5 6

■ ■ ■ □ □ □

A에게 1·2번을, B에게 3번을 부여했습니다. 그리고 A 프로세스가 종료되어 메모리를 반환합니다.

□ □ ■ □ □ □

이 다음에 C라는 프로세스가 4만큼의 메모리를 요청합니다.

원래대로라면 메모리는 순서대로 부여해야 합니다. 실제 물리 주소를 곧바로 프로세스에 줬다면 포인터가 맨 마지막 주소를 가리키고 있을 테니까요.

그럼 지금 상황에서는 크기 4의 메모리를 줄 수 없습니다. 뒤에 3밖에 남지 않았으니까요.

그래서 MMU는 빈 공간을 활용해 C에게 4만큼의 메모리를 부여합니다.

1 2 3 4 5 6

■ ■ ■ ■ ■ □

이렇게 되면 B 프로세스가 3번을, C 프로세스가 1·2·4·5번을 갖게 됩니다.

"어? 그럼 C 프로세스는 중간에 한 칸 건너뛰는데요?"

그래서 가상 메모리가 힘을 발휘합니다. 실제 C가 받은 주소는 1, 2, 4, 5이지만 MMU가 이를 바꿔치기해서 1, 2, 3, 4로 보이게 만듭니다.

C 프로세스 입장에서는 자기에게 부여된 메모리가 연속적이라고 인지하고 동작하지만, 실제 물리 주소는 떨어져 있는 것이죠.

이것이 가상 메모리의 개념입니다. GC에서는 조금 달라서, Compaction 작업이 추가되어 단편화된 메모리를 왼쪽으로 한 번에 몰아넣고 빈 공간을 억지로 만드는 "청소"에 가깝습니다. 그래서 STW가 생기는 것이고요. 아무튼 이번 포스팅은 GC와는 관계없으니 넘어가겠습니다.

vLLM의 등장

vLLM은 이런 착안에서 탄생했습니다. 기존 CPU 기반 프로세스들은 CPU 메모리(DRAM)를 쓰면서도 OS가 가상 메모리로 알아서 잘 처리해 주니 신경 쓸 필요가 없었죠.

LLM은 VRAM을 DRAM처럼 쓰는 경향이 있으니, DRAM에 적용했던 가상 메모리를 VRAM에도 적용하자. 이것이 컨셉이었습니다.

그리고 이는 Attention 연산을 Paging 처리한다 하여 PagedAttention이라는 개념으로 등장했습니다. 파장은 생각보다 컸습니다. 20TPS가 나오던 답변이 갑자기 50TPS를 찍으니 당황할 수밖에 없었죠.

처리량은 또 어떻고요. 갑자기 두 배, 세 배로 늘어나기 시작했습니다.

그런데 개발자들은 이 현상을 잘 압니다. 처음 도커에 열광하던 개발자들도 성능에 놀랐지만, 결국 써 보니 수백, 수천 개의 컨테이너를 관리하기가 힘들어졌다는 것을요. 뒤이어 쿠버네티스가 등장했지만 어마어마한 운영 복잡도 탓에 쉬이 도입하지 못하게 됐고요.

"이런 장점만 있는 기술이 존재할 리 없어…! 뭔가 단점이 있을 거야. 사이드 이펙트가 분명 어딘가에 있을 거야…!"

vLLM에도 부작용이 있었습니다. 그건 바로 가상 메모리의 부작용에서 찾을 수 있었죠. MMU는 논리 주소와 물리 주소를 연결하기 위한 Paging Table을 가지고 있는데, 메모리를 할당할 때마다 이 테이블을 조회해야 하는 추가 연산이 필요했고 이것이 오버헤드로 이어졌습니다.

하지만 이는 OS가 지고 가야 할 숙명 같은 것이었죠. 오버헤드 때문에 가상 메모리를 포기할 수는 없었으니까요. 그래서 가상 메모리는 좋은 개념이었지만 오버헤드라는 사이드 이펙트를 남긴 채 우리와 함께 살아가고 있습니다.

vLLM을 도입해도 마찬가지로 테이블 조회 연산이 필요했습니다. 그런데 희한하게도 이 부작용이 크게 대두되지 않았습니다. 왜일까요?

1. VRAM을 쓰는 것이 CPU가 아니라 GPU라서

CPU는 정수 계산에 특화되어 있고 병렬 연산에는 쥐약입니다. 그래서 CPU가 쓰는 DRAM에서 가상 메모리 테이블을 조회하는 것은 분명 눈에 띄는 오버헤드였습니다.

하지만 VRAM에 가상 메모리 개념을 도입하니 문제가 크게 보이지 않았습니다. 레이턴시는 분명 존재했지만, VRAM을 쓰는 주체가 GPU라는 게 크게 달랐죠.

GPU는 소수점 계산에 능하고 병렬 연산에 최적화되어 있습니다. 테이블을 조회하는 동안 GPU는 다음 연산을 준비하거나 다른 연산을 하고 있으면 되기 때문에, 실질적으로 체감하는 레이턴시는 매우 작았던 것입니다.

2. 레이턴시가 뭐가 중헌디

안 그래도 병렬 연산에 특화된 GPU인데, 거기에 VRAM 가상 메모리까지 도입하니 처리량이 어마어마하게 늘었습니다.

개발자들이 환장하는 두 단어, "레이턴시"와 "처리량".

vLLM을 도입하니 같은 하드웨어로 두 배, 세 배 많은 사용자를 받을 수 있었으니 엄청난 이득이었죠. 거기다 레이턴시는 눈에 보이지도 않으니, 개발자들이 너도나도 vLLM을 도입하게 된 계기가 되었습니다.

vLLM의 유일한 단점

NVIDIA 그래픽카드만 지원한다는 게 가장 마음 아픈 단점입니다. 저는 요즘 맥으로 개발하고 있어서 GGUF밖에 못 쓰는데, NVIDIA 그래픽카드가 없어 vLLM을 못 쓴다는 게 천추의 한입니다 ㅜㅜ

그래서 병렬로 실행하고 싶은 연산이 있으면 llama.cpp 서버를 띄워서 처리하고 있는데 성능이 영 시원찮네요.

프로젝트에 들어가면 저도 H100이나 H200을 만져 볼 기회가 있으면 좋겠습니다. ㅜㅜ

마치며

vLLM은 오늘날 AI 엔지니어링에서 가장 핫한 LLM 서빙 도구가 되었습니다. 나중에는 NVIDIA의 생태계가 과거 윈도우 운영체제에 맞먹는 위상이 된 현 상황을 두고 구구절절 푸념하는 글을 쓸 날이 있을 것 같네요.

좋다 좋다 말로만 들었지 실제로 써 보고 체감한 게 아니라서, vLLM에 대한 개인적인 평가를 글에 녹이지 못한 것이 조금 아쉬운 포스팅입니다. 혹시 모르죠, 저희 회사에도 H200이 들어오게 될지. 으흐흐

오늘 포스팅은 여기까지 하겠습니다. 오늘도 긴 글 읽어 주셔서 감사합니다. 새해 복 많이 받으세요!

기술 발전 순서로 알아보는 KV Cache

안녕하세요. 이번에는 KV Cache를 공부한 내용을 정리해 볼까 합니다. 요즘 AI 엔지니어링을 하면서 가장 신경 써야 하는 것이 바로 이 KV Cache가 점유하는 메모리를 관리하는 일인 것 같습니다. 그래서 이번에는 KV Cache란 무엇이고, 이를 효율적으로 관리하기 위해 탄생한 기술들을 정리해 보겠습니다.

Attention이란?

KV Cache를 알아보기 전에 Transformer의 Attention 연산을 짚고 넘어가겠습니다. Attention 메커니즘에서 데이터를 처리할 때(Inference 할 때) 각 토큰은 세 가지 역할을 부여받습니다.

• Query (Q) : 내가 지금 찾고 있는 정보가 무엇인가? (질문)

• Key (K) : 내 정보는 이런 거야, 나랑 매칭해 볼래? (인덱스)

• Value (V) : 나랑 매칭됐으면 이 값을 가져가! (값)

백엔드에서 `Map<Key, Value>`에 `get(Key)`로 데이터를 가져오는 것과 비슷합니다. 모델은 Query를 던져 과거의 Key들과 대조해 보고, 연관성이 높은 순서대로 Value를 긁어옵니다.

이 과정을 Attention이라고 부릅니다. 이 기본 개념에서부터 KV Cache를 설명해 보겠습니다.

MHA (Multi Head Attention)

기본적인 Transformer의 Attention 연산은 MHA를 따릅니다. 이때 생성된 행렬 값들이 메모리에 저장되는데, 이를 우리가 KV Cache라고 부릅니다.

여기서 새로 등장하는 개념이 헤드입니다. 헤드는 Q, K, V에 매칭되는 개념으로 쿼리 헤드, K 헤드, V 헤드 이런 식으로 부릅니다. MHA는 가장 원초적인 형태의 Attention 연산으로, Q:K:V가 1:1:1을 이루고 있었습니다.

조금 더 직관적으로 이해하면 이렇습니다.

• Q : 나한테 명령하는 상사

• K : 실제로 일 처리하는 사람

• V : 산출물

MHA는 일을 시키는 사람과 실제로 처리하는 사람이 1:1이었기 때문에 답변 품질이 굉장히 좋았습니다. 다만 그만큼 회사에 자리를 많이 차지하는 문제가 있었고, 회사가 좁아터지게 됐죠.

KV 행렬 값을 메모리에 올려 두고 토큰을 생성할 때마다 사용하니 연산은 효율적이었지만, 메모리를 잡아먹는 속도가 어마어마했습니다.

그래서 이 방법은 금방 잊혀 갔습니다.

MQA (Multi Query Attention)와 GQA (Grouped Query Attention)

MHA가 KV Cache를 무지막지하게 잡아먹어서 고안된 방법들입니다.

MQA

쿼리 헤드가 8개일 때(상사가 8명일 때) KV 헤드는 단 한 개인 구조(실제 일 처리하는 사람은 한 명)를 말합니다. 8개의 서로 다른 쿼리가 하나의 인덱스와 하나의 데이터를 보고 각자 필요한 것을 찾아가는 방식이죠.

장점은 KV Cache 용량이 압도적으로 줄어든다는 것이지만, 성능도 압도적으로 낮아진다는 단점이 있습니다. 상사가 8명인데 각자 다른 일을 시킨다고 생각하면 정말 끔찍하네요.

KV 헤드가 기억할 수 있는 컨텍스트가 부족하고, KV 헤드 하나가 감당할 수 있는 양도 넘어서기 때문에 답변 품질에 상당한 악영향을 미쳤다고 볼 수 있습니다.

GQA

MHA와 MQA의 절충안으로, 쿼리 헤드를 그룹으로 묶어 그룹 하나당 KV 헤드를 하나씩 할당하는 방법입니다. 즉 일을 시키는 상사가 2명, 처리하는 사람이 1명인 구조죠.

MQA에 비하면 메모리는 더 먹지만 답변 품질이 괜찮아진다는 장점이 있습니다. 또한 MHA에 비해서는 KV Cache를 덜 먹는다는 것이 또 다른 장점이었죠.

현재 GQA는 메타부터 구글까지 미국 빅테크들이 널리 채택하는 KV Cache 관리 전략이 되었습니다.

이건 Gemma-3-27b-it의 `config.json`을 가져온 것입니다.

잘 보시면 num_attention_heads가 32, num_key_value_heads가 16인 것을 확인할 수 있습니다. 쿼리 헤드는 32개인데 KV 헤드는 16개라는 뜻이니, 앞선 예시처럼 상사 2명에 부하 1명인 구조입니다.

MLA (Multi-Head Latent Attention)

제가 굳이 "미국 빅테크"라고 언급한 이유가 있습니다. 중국은 그래픽카드가 부족해서 온몸 비틀기를 할 수밖에 없었거든요. 메모리 용량이 부족한데 KV Cache가 미친 듯이 늘어나는 것을 막아야 했으니까요.

MLA는 헤드를 여러 개 쓰는 것은 동일하지만, 저수준 잠재 벡터(Latent Vector)로 압축해 차원을 줄인 뒤 필요할 때 다시 업프로젝션(Up-Projection)해서 쓰는 방법입니다.

수학 얘기가 조금 나오는데… 사실 이 부분을 포스팅하는 게 맞나 싶긴 합니다만, 이어 가 보겠습니다.

우리가 구하려는 Attention 값은 사실 머신러닝에서도 많이 등장하는 Score입니다. 이걸 Attention Score라고 부르죠. 이를 구하는 방법은 Query와 Key를 내적하는 것입니다.

고등학교 때 기하와 벡터를 공부해 보셨다면 익숙한 수식일 겁니다.

여기서 중요한 건 코사인 값입니다.

두 값을 내적했는데 코사인 값이 1이어서(세타가 0이어서) 내적 값이 크게 나왔다면, 이 두 벡터는 같은 방향을 가리키고 있다는 뜻입니다. 컴퓨터공학적으로 해석하면 유사도가 매우 높다고 판단할 수 있는 것이죠.

반대로 코사인 값이 0에 가까워질수록(세타가 90도에 가까워질수록) 내적 값이 0에 수렴합니다. 두 벡터가 수직을 가리키고 있다는 뜻이고, 이는 유사도가 없다고 판단합니다.

자, 이제 본격적으로 들어가 보겠습니다.

Attention Score는 쿼리와 Key를 내적하는 것이라고 했죠? MLA에서는 이 내적에 약을 칩니다. 벡터의 차원을 낮추는 약을 치는 거죠. 이를 "다운프로젝션을 행렬곱했다"라고 표현합니다. 그리고 필요할 때 업프로젝션을 행렬곱해서 원상복구하는 것이죠.

그런데 이렇게 업프로젝션을 하면 오버헤드가 생기지 않을까요? 결론부터 말하면 생기지 않습니다.

왜일까요? 벡터를 원래대로 되돌리는, 복원된 Key의 수식은 다음과 같습니다.

다른 건 안 보셔도 됩니다. `W`라고 쓰인 것이 업프로젝션 행렬이고, 이를 곱한 결과값 `k`가 Key입니다.

그리고 Attention Score는 쿼리와 Key의 내적이라고 했으니 수식은 이렇게 됩니다.

이것도 다른 건 안 보셔도 됩니다. 원래 쿼리에 업프로젝션한 Key를 곱했다 정도로 생각하시면 됩니다.

직관적으로 이해하기 위해 예시를 들어 보겠습니다.

100만 명이 마라톤에 출전했다고 가정해 봅시다. 마라토너들의 가슴팍과 등짝에는 자신의 참가 번호가 쓰여 있습니다. 그런데 이 숫자를 한 번에 다운사이징해 버립니다. (이유는 묻지 말아 주세요.) 그래서 몸에 붙은 숫자가 보이지 않게 됐습니다.

결승점에 도착한 선수들의 번호로 순위를 매겨야 하는데, 진행위원이 글자가 너무 작아 보이지 않는다고 하소연하기 시작합니다.

지금 저 Attention Score 수식은 100만 명 전원에게 "다시 숫자 크기를 키워 오세요!"라고 말하는 것과 같습니다. 그럼 정말 어마어마한 오버헤드가 생기겠죠.

그래서 딥시크는 행렬곱의 결합법칙을 이용해 이 문제를 해결했습니다. 곱셈의 순서를 바꿔도 값이 같다는 점을 이용한 것이죠.

이게 무슨 의미냐면, 100만 명 전원에게 숫자를 키워 오라는 게 아니라 진행위원에게 현미경 하나를 쥐어 준 것과 같습니다. 백엔드식으로 해석하면, 100만 개의 행을 풀스캔하는 것보다 앞에서 인덱싱해 찾는 것이 빠른 것과 같은 이치입니다.

다시 한글로 풀어 보겠습니다.

스코어 = 쿼리 × 업프로젝션 행렬 × 키

원래는 이랬습니다. 다운프로젝션된 행렬값과 키를 곱한 것을 KV Cache에 저장해 두었다가, 이를 업프로젝션 행렬로 복원한 뒤 쿼리와 일일이 비교해야 했죠.

즉 업프로젝션 행렬 × 키를 먼저 계산하고 거기에 쿼리를 곱해야 했습니다. 그런데 결합법칙으로 쿼리 × 업프로젝션 행렬을 먼저 곱해 버립니다. 그러면 업프로젝션된 쿼리 하나가 튀어나오고, 여기에 키만 곱하면 되므로 연산량이 0에 수렴하게 되는 것이죠.

0은 아니고 '0에 수렴'인 이유는, 이 예시에서 연산량이 100만분의 1이 된 것이지 엄연히 0은 아니기 때문입니다.

이를 행렬 흡수라는 이름으로 딥시크가 적용했습니다.

RoPE라는 변수

최근 LLM들은 토큰의 위치를 알기 위해 RoPE(Rotary Positional Embedding)라는 기술을 씁니다. 그런데 RoPE는 벡터를 회전시켜야 하기 때문에 행렬 흡수 수식과 수학적으로 섞일 수 없었습니다. 회전 후 곱하는 것과 곱한 후 회전하는 것의 결과가 다르기 때문이죠.

그래서 딥시크는 쿼리와 키를 두 부분으로 쪼개서 계산합니다.

첫 번째 항은 압축된 캐시를 이용해 효율적으로 문맥을 검색하고, 두 번째 항은 토큰의 위치를 찾기 위해 별도로 관리되는 정보 검색용이라고 생각하시면 됩니다. 이 둘을 더한 값으로 Attention Score를 구하는 것이죠.

DSA (DeepSeek Sparse Attention)

최근 DeepSeek V3에서는 DSA라는 기술이 추가로 공개되었습니다.

기본적인 Attention 연산은 문장이 길어질수록 연산량이 제곱으로 늘어납니다. 그래서 문맥이 128k만 되어도 MLA로 감당할 수 없는 문제가 생깁니다. GPU가 부족한 중국은 KV Cache를 방망이 깎는 노인처럼 깎기 시작했습니다.

• Lightning Indexer : 모든 토큰을 다 대조하는 게 아니라, FP8 정도의 정밀도를 가진 아주 가벼운 인덱서를 먼저 돌립니다.

• Top-K Selection : 인덱서가 "이 쿼리에는 이 100개의 과거 토큰(Key-Value)이 가장 중요하다"고 Top-K를 찍어 주면, 모델은 딱 그 부분만 정밀하게 계산합니다.

• 그 결과 연산 복잡도가 제곱에서 선형으로 바뀌었습니다.

DSA와 MLA는 서로 대체되는 기술이 아니라, MLA 구조 위에 구현된 상위 레이어입니다. MLA는 데이터를 저차원으로 압축해 메모리를 아끼는 기술이고, DSA는 압축된 데이터 중에서 현재 질문과 가장 관련 있는 것들만 골라 연산량을 아끼겠다는 취지죠.

딥시크의 최신 모델(V3.2)은 여기서 한발 더 나아가 MQA와 DSA를 합치려는 움직임도 보입니다. KV 헤드를 공유하는 MQA와 선택적 연산인 DSA를 합쳐 메모리 사용량을 폭발적으로 아끼려는 것이죠.

딥시크는 MLA와 DSA를 적용하면서, 문장이 아무리 길어져도 첫 토큰이 나오는 속도인 TTFT를 거의 일정하게 유지할 수 있게 되었습니다.

어른들의 사정?

왜 이렇게 좋아 보이는 기술이 중국 쪽에서 우수수 쏟아지는 걸까요? 중국이 후발주자라 혁신적이지 않으면 살아남지 못해서? 중국인이 똑똑해서?

역시 앞서 언급했듯 부족한 그래픽카드가 한몫했을 것입니다. 그래픽카드 서버가 부족해서 나오는 온몸 비틀기에 가까운 것이죠.

미국은 상대적으로 풍부한 GPU 서버를 공급받을 수 있었고, 중국은 대중 무역 제재로 그러지 못했습니다. 이 차이가 기술적 차이를 만든 것이죠.

LLM 모델이 차지하는 메모리보다 KV Cache가 점유하는 메모리가 훨씬 더 큰(사실 말도 못 할 정도로 차이가 납니다) 상황에서, KV Cache를 줄이는 것은 곧 필요한 서버 수가 줄어드는 것이고, 그것이 곧 중국이 살아남기 위한 방법이 되는 셈입니다.

반면 GQA가 표준이 되어 미국 빅테크들이 이를 채택하는 이유는, LLaMA나 GPT 시리즈에서 수조 개의 파라미터를 학습시키고 안정성까지 검증받았기 때문에 굳이 MLA로 넘어갈 필요가 없었기 때문입니다.

또한 MLA는 벡터를 압축하고 다시 복원하는 복잡한 로직이 필요하고, 이 과정은 커널을 뜯어고쳐야 하는 수준입니다. 중국에서도 CUDA 커널 말고 새로운 커널 시스템이 필요했고, 딥시크는 Triton이라는 이름으로 새 커널을 만들면서까지 개발했습니다.

조금 다른 얘기지만 며칠 전 등장한 카카오의 Kanana-2가 딥시크 아키텍처를 적용했다고 합니다. 이는 커널 엔지니어링이 가능한 수준을 갖추었다고 해석해도 되고, 한국의 AI 개발 실력이 빅테크를 중심으로 멈춰 있지 않다는 것을 보여 주는 사례로 볼 수 있습니다. 한국 역시 그래픽카드가 부족했고, 그래서 이런 기술적 의사결정을 하지 않았나 싶습니다.

미국 빅테크 입장에서는 커널 엔지니어링까지 해야 하고 기존에 학습시킨 데이터가 뭉개질 수도 있는, 리스크 있는 일을 굳이 하지 않은 것이겠죠. 그것보다 그냥 그래픽카드 서버 한 대를 더 박아 넣는 게 경제적으로 이득이었을 수도 있습니다.

그래서 딥시크가 개발한 MoE는 저수준 개발이 필요 없었기에 대부분의 빅테크가 적용한 반면, MLA처럼 저수준 개발이 필요한 것들은 선뜻 적용하지 않은 것으로 보입니다.

하지만 미국 빅테크 입장에서도 이를 적용하지 않을 이유는 없습니다. 10만 대로 처리하던 것을 1만 대로 처리할 수 있다면 유지비도 줄고 서버 공간 같은 물리적 비용까지 줄어드니, 오히려 적용하지 않는 것이 손해로 보이기도 합니다.

미국 빅테크들이 이런 기술이 없어서 못 만들었다기보다는, CUDA를 포기해야 하는 상황이라는 점이 걸립니다. 만약 CUDA가 갑자기 MLA를 지원한다면 빅테크들에게 더 이상 많은 그래픽카드가 필요하지 않게 되고, 이는 엔비디아의 실적 하락으로 이어지겠죠. 과연 엔비디아가 이를 지원해 줄까 싶습니다.

그럼에도 미국 빅테크들 역시 결국 비용을 줄이고 싶어 할 겁니다. 구글이 최근 TPU라는 칼을 빼 들며 엔비디아에 도전장을 내밀었죠. 추론에 특화된 AI 칩인 TPU가 구글의 전성비를 크게 높여 비용을 많이 아껴 주었습니다. 저는 개인적으로 구글이 새로운 커널을 만들어 MLA까지 적용하는 그림을 조심스럽게 바라고 있습니다.

마치며

이번 포스팅에서는 KV Cache를 기술 발전 순서에 맞춰 다뤄 보고, 끝으로 제가 생각한 어른들의 사정까지 이야기해 봤습니다. 공부할 때 수학 수식에 압도당해서 어떻게든 이해하려고 엄청나게 고생했습니다. 이번 포스팅을 계기로 선형대수학을 공부해야 하나 싶은 생각도 들었네요.

이번에 공부한 내용이 제 머릿속에 오랫동안 남아 있기를 기원하면서… (ㅜㅅㅜ) 이번 포스팅은 여기서 줄이겠습니다. 긴 글 읽어 주셔서 감사합니다. 오늘도 즐거운 하루 되세요!

Transformer 알고리즘은 과거와 다르게 어떻게 발전했을까?

안녕하세요. 요즘 AI 엔지니어링 업무를 하면서 관련된 것들을 공부하니 더 재밌는 것 같네요. 이번 포스팅에서는 Transformer 논문이 출판됐을 당시부터 있었던 근본 개념들을 알아보고, 그때의 한계를 지금은 어떻게 극복했는지를 중점으로 정리하겠습니다.

LLM을 논하기 전에 가장 중요한 개념이 바로 Transformer입니다. 개발자가 아니어도 AI에 조금만 관심이 있다면 한 번쯤 들어 봤을 내용이죠. 저는 이론 없는 실전은 기반 없이 무작정 높게만 쌓은 건축물이라는 생각이 강해서, 지금 당장 실전에서 써먹지 못하더라도 알고 안 쓰는 것과 모르고 안 쓰는 것은 차이가 있다고 보는 주의입니다.

AI 엔지니어링은 실제 그래픽카드 서버가 없으면 제약이 많아 실전을 많이 경험하기 어렵기도 하고, 그렇기에 이론이 더더욱 중요한 것 같습니다.

서론이 길었습니다. 바로 들어가 보죠!

들어가기 전에

이번 포스팅은 다음 순서를 따릅니다.

• Attention 연산이란?

- 유사도 계산 : 쿼리와 키의 관계에 초점을 맞춰 서술합니다.

- 스케일링 : 모델의 파라미터가 커짐에 따라 보정 작업이 들어가는 과정을 이야기합니다.

- Softmax : 행렬 계산 결과를 0~1 사이의 확률로 볼 수 있게 하는 과정을 이야기합니다.

- 가중합 : 가중치와 실제 값을 매칭해 벡터에 문맥을 부여하는 과정을 이야기합니다.

• 연산 부하를 줄여라! Flash Attention

Attention 연산의 가장 큰 문제는 행렬 값을 계산하기 위한 연산량이 어마어마하다는 것입니다. 현재 모델들이 이를 줄이기 위해 쓴 공학적 트릭을 잠깐 짚고 넘어갑니다.

• Positional Encoding

벡터가 널린 사막에서 맥락이라는 방향을 유지하기 위해 어떤 방법을 쓰는지, Transformer 논문이 나왔을 때와 지금은 어떻게 다른지, 그때의 한계를 어떻게 기술적으로 해결했는지 서술합니다.

• Encoder와 Decoder

초기 Transformer에는 맥락을 파악하는 Encoder와 텍스트 생성을 담당하는 Decoder가 있었습니다. 요즘 모델에서 자주 보이는 Decoder-Only는 Encoder가 없으면 어떤 문제가 생기고 그것을 어떻게 해결했는지 다룹니다.

여기서 중점적으로 볼 부분은 유사도 계산과 스케일링입니다. Softmax와 가중합은 깊이 들어가면 수학 공식이 난무해서 도저히 쓸 수 없었습니다.

그래서 이번엔 다른 포스팅과 다르게 조금 가볍게 갈 예정입니다. 이제 본격적으로 들어가 보시죠!

Attention 연산이란?

Attention 연산은 Transformer가 탄생한 최초의 논문에서부터 언급되었을 만큼 근본 중의 근본이고, 아직도 많은 모델이 이 연산을 기반으로 텍스트를 생성합니다.

수식은 다음과 같습니다.

수학적인 이야기는 하지 않고 공학적 해석이 주를 이루니 걱정하지 않으셔도 됩니다. (저도 수학 겁나 못해요)

먼저 쿼리와 키를 내적하는 "유사도 계산" 파트가 있습니다. 그리고 벡터의 차원이 커지면 Softmax 연산에 노이즈가 생길 수 있어 이를 억제하는 "스케일링" 구간이 있죠. 이어서 모델이 알아먹을 수 있게 확률로 표현하는 "Softmax" 단계, 마지막으로 맥락과 값을 행렬곱하는 "가중합" 단계가 있습니다.

하나씩 천천히 살펴보죠.

유사도 계산

첫 단계는 쿼리와 키를 내적해 유사도를 구하는 것입니다. 내적이 어떻게 유사도로 이어지는지 보면, 답은 두 위치 벡터의 각도입니다.

내적은 두 벡터의 크기와 그 사이의 코사인 값을 곱해 이루어집니다. 코사인 함수의 특성상 사잇각이 0도면 코사인 값이 최대가 되어 내적값도 최대가 되고, 90도가 되면 코사인 값이 0이 되어 내적값도 0으로 떨어집니다.

따라서 두 벡터의 내적값은 이 둘이 문맥상 얼마나 유사한지 판단하는 척도가 됩니다.

Attention 연산은 쿼리(Q), 키(K), 값(V) 세 개로 이루어져 있고 각각의 의미는 이렇습니다.

• Query : "나는 이런 정보를 찾고 있어!"라는 요구사항입니다.

• Key : "나는 이런 특징을 가진 정보야"라는 인덱스와 같습니다.

• Value : "나는 실제로 이런 값이야"라는 값 그 자체입니다.

그런데 유사도를 이야기하면서 왜 하필 쿼리와 키를 내적하는 걸까요? 이 둘에 무슨 공학적 의미가 있길래요?

쿼리와 키를 내적한다는 것은 내가 던지는 질문과 내가 실제로 찾는 대상 사이의 연관 관계를 내적값의 크기로 판단한다는 뜻입니다.

• Query : "제가 찾고 있는 건 빨갛고 동그란 과일입니다."

• Key : "저는 사과를 가리키고 있습니다." / "저는 바나나를…" / "저는 수박을…"

• Value : "사과" / "바나나" / "수박"

쿼리와 키를 내적하면 다음과 같은 연관성을 얻습니다.

• 빨갛고 동그란 과일과 사과의 관계

• 빨갛고 동그란 과일과 바나나의 관계

• 빨갛고 동그란 과일과 수박의 관계

즉 내가 던지는 질문과 내가 찾는 값 사이의 연관성을 찾기 위해 쿼리와 키를 내적하는 것이죠.

스케일링

벡터의 차원 수가 커지면(모델의 파라미터가 커지면) 내적값의 절댓값도 확률적으로 커집니다. 그런데 내적값이 너무 커지면 뒤에 나올 Softmax에서 문제가 생깁니다. Softmax는 입력이 크면 결과도 크고 입력이 작으면 결과도 작아지는 특징이 있어서, 벡터가 커지면 결과가 너무 크거나 너무 작은 극단적인 형태를 띠게 되거든요.

이게 왜 문제일까요? 값이 극단적으로 몰리면 Gradient(기울기)가 0에 수렴하는 문제가 생깁니다.

기울기가 0이 된다는 건 수학적으로 미분값이 0이 된다는 뜻입니다. 모델을 학습시킬 때는 역전파를 쓰고, 이때 손실 함수(L)를 가중치(W)로 미분한 값을 이용해 점수를 매기기 때문에 문제가 됩니다.

역전파는 딥러닝에서 파생된 개념으로, 추후 따로 포스팅할 예정입니다. 우선은 모델을 학습시킬 때 쓰는 방법론이라고만 이해하고 넘어가도 될 것 같습니다.

여기서 손실 함수와 보상 함수 개념이 등장하니 짚고 넘어가죠.

• 손실 함수 : 연관 없는 것들에 감점을 하는 방식입니다. 더하지는 않고 감점만 하기 때문에, 점수가 높은 것은 연관성이 높다고, 낮은 것은 연관성이 낮다고 판단하게 됩니다.

• 보상 함수 : 연관 있는 것들에 가점을 하는 방식입니다. 깎지는 않고 가점만 하기 때문에 모델이 어느 방향으로 가야 하는지 방향성을 제시해 줍니다. (이 방향으로 가면 점수를 더 많이 주네? 앞으로 여기로 가야겠다.)

손실 함수는 주로 딥러닝에서, 보상 함수는 주로 강화학습에서 쓰이는 개념이라 조금 다릅니다. 그래도 점수를 깎거나 더해서 모델이 어느 방향으로는 가지 말아야 할지, 어느 방향으로는 가야 할지 알려 주는 개념이라고 이해하시면 됩니다.

손실 함수를 가중치로 미분한 값이 0에 가까워지면, Softmax를 통과할 때 작은 값이 한없이 더 작아지는 효과가 생깁니다. 그렇게 기울기가 0이 되어 버리면 모델은 "수정할 방향을 잃어버렸다"고 판단하고 더 이상 똑똑해지지 않은 채 멈춰 버립니다. 이를 Dead Neuron, 즉 죽어 버린 뉴런이라고 부릅니다.

그래서 내적값의 분산을 일정하게 유지해 Softmax가 안정적인 기울기를 갖도록 표준화하는 작업, 그것이 스케일링입니다.

Softmax

Softmax는 쉽게 말해 유사도를 확률로 나타낸 것입니다.

유사도는 쿼리와 키의 내적이라고 했는데, 내적을 하면 결괏값이 벡터 형태로 존재합니다. (내적의 결과가 스칼라라는 것과는 조금 다른 이야기입니다.) 여러 차원에 흩뿌려진 데이터는 LLM이 이해하기 힘들기 때문에, 이를 0과 1 사이의 확률로 나타내는 것이죠.

이때 Softmax는 내부적으로 자연상수 e의 거듭제곱 꼴을 씁니다. 이는 큰 값은 더 크게, 작은 값은 더 작게 만드는 효과가 있습니다. 그 결과 시퀀스 안의 여러 단어 중 현재 쿼리와 정말 관련 있는 단어에만 점수를 몰빵해 주고, 관련 없는 단어는 0에 가깝게 죽어 버리는 효과가 생깁니다.

가중합

이전 단계가 "어떤 단어에 얼마나 집중할지"의 확률을 구한 것이라면, 이제 이 확률을 실제 값인 V와 결합해 실제 정보를 그 확률만큼 가져와야 합니다.

각 단어의 Value 벡터에 Softmax로 구한 가중치를 행렬곱해 모두 더합니다. 만약 "사과"라는 단어의 쿼리가 "맛"이라는 키와 0.9만큼 연관되어 있다면, "사과"의 최종 출력 벡터에는 "맛"이 가진 Value 정보가 90퍼센트 섞이게 됩니다.

최종적으로 가중합을 통해 나온 벡터는 단순한 단어 벡터가 아니라, 주변 문맥 정보가 연관성만큼 가중치로 섞인 "문맥이 반영된 벡터"가 됩니다.

Flash Attention

Attention의 가장 큰 문제는 "모든 단어가 모든 단어를 바라본다"는 것입니다.

문장의 길이가 N이라면 쿼리와 키를 행렬곱하는 순간 N × N의 Attention Map이 형성됩니다. 문장이 천 개의 단어로 이루어져 있다면 1,000 × 1,000으로 100만 번, 만 개라면 1억 번… 이런 식이죠.

연산 횟수도 병목이지만 더 끔찍한 건 메모리 병목입니다. 이 거대한 행렬을 GPU 메모리에서 썼다 읽었다 하는 과정에서 어마어마한 병목이 생깁니다. 이를 "폰 노이만 병목"이라고도 부르죠.

이때 등장하는 개념이 바로 Flash Attention입니다.

Flash Attention은 알고리즘 자체를 바꾼 것이 아니라 컴퓨팅 아키텍처 최적화로 이 문제를 해결했습니다. N × N 행렬을 한 번에 처리하지 않고, GPU의 아주 작지만 빠른 내부 캐시 메모리인 SRAM 안에서 계산을 끝낸 뒤 최종 결과만 밖으로 내보내는 방법을 택했죠.

느려터진 메모리에서 행렬 값을 계산하는 것보다 SRAM에서 필요한 것만 계속 받아다 계산하는 것이 더 빠르기 때문에 채택된 방법이라고 볼 수 있습니다.

Positional Encoding

Attention 연산이 병렬 처리에 유리했던 이유는 "순서와 상관없이 한 번에 계산하기" 때문이었습니다. 그런데 언어는 역설적이게도 순서가 가장 중요하죠.

"개가 사람을 물었다"와 "사람이 개를 물었다"는 의미 차이가 하늘과 땅 차이니까요.

이 문제를 해결하기 위해 각 단어의 "임베딩 벡터"에 해당 단어의 "위치 정보 벡터"를 더해 주는 방법을 씁니다.

이 작업은 Attention 연산을 시작하는 맨 앞단에서 적용되며, 흐름은 다음과 같습니다.

• Tokenization : 문장을 토큰 ID로 쪼갭니다. ["안녕", "하세", "요"] → [101, 2045, 312]

• Embedding Lookup : 각 정수 ID를 거대한 임베딩 테이블에서 찾아 의미를 가진 고밀도 벡터로 바꿉니다. [v_안녕, v_하세, v_요]

• Positional Encoding : 각 위치에 맞는 위치 벡터를 생성해 더해 줍니다. [v_안녕+P_1, v_하세+P_2, v_요+P_3]

• 이제 순서 정보가 포함된 이 벡터들이 Attention 레이어로 진입합니다.

위치 정보를 더해 준다는 건 알겠는데, 그 위치 정보는 어디서 가져오는 걸까요?

고전적으로는 sin, cos 함수를 이용했습니다. Transformer 최초 논문에서는 서로 다른 주기의 사인·코사인 함수를 사용해, 각 위치마다 서로 다른 주파수의 삼각함수 값을 조합함으로써 모든 위치가 고유한 벡터를 갖게 했습니다.

삼각함수의 덧셈정리 덕분에 모델은 현재 단어로부터 k만큼 떨어진 단어와의 상대적 거리를 선형 변환으로 쉽게 알 수 있었죠.

그런데 이렇게 했더니 문제가 생겼습니다. 모델을 2k 토큰으로 학습했는데 사용자가 20k짜리 코드를 들고 오면 어떻게 될까요?

모델 입장에서는 "나는 2k까지밖에 모르는데 그다음은 어떻게 하지?"가 됩니다. 이렇게 되면 할루시네이션이 폭발해 이상한 답변을 하게 되니, 이를 해결해야 했습니다.

그래서 요즘은 RoPE라는 방법을 씁니다.

RoPE

1번 단어, 2번 단어, 3번 단어가 있다고 가정해 봅시다. 이때 고정된 각도를 임의로 먼저 세팅합니다. 이 각을 세타라고 하죠.

그럼 1번 단어는 1세타, 2번 단어는 2세타, 3번 단어는 3세타, n번 단어는 n세타로 표현할 수 있습니다. 1번 단어와 n번 단어의 상대적 위치는 (n−1)세타가 되고요.

이게 무슨 의미일까요? 보통 모델을 학습시킬 때는 파라미터가 어마어마하게 큰 모델을 쓰는데, 이를 큰 토큰으로 학습시킬 수는 없습니다. 그래서 보통 2k 정도의 토큰으로 학습하는데, 앞서 말했듯 사용자가 30k짜리 토큰을 들고 오면 난처해지죠.

하지만 RoPE를 쓰면 2k 이후에도 2001번은 2001세타, 2002번은 2002세타… 이런 식으로 상대적 위치를 계속 알 수 있게 됩니다.

그래서 요즘 LLM들이 어마어마하게 많은 Input 토큰을 받을 수 있는 것입니다.

그런데 이상하죠. sLLM 같은 모델로 개발해 보면 입력 토큰이 제한된 경우가 대부분입니다. 이건 어떻게 된 걸까요?

sLLM에도 같은 방법론이 적용되어 있습니다. 다만 이는 백엔드에서 Connection Pool이나 JVM Heap Memory를 제한하는 것과 같은 이치입니다. 하드웨어가 수용할 수 있는 토큰과 모델이 이해하는 토큰은 다르기 때문이죠.

아무리 Flash Attention을 써도 문장이 길어지면 KV Cache가 차지하는 메모리가 선형적으로 늘어나고, 작은 sLLM이 감당 가능한 영역을 넘어서면 OOM이 발생합니다. 또한 모델이 학습한 토큰 범위 이후까지 이해할 수 있다 해도, 이를 정확히 추론하는 능력은 별개의 문제입니다.

주로 8k로 학습했다면 32k가 들어올 때 이해는 하지만 정확도가 떨어지는 현상이 생깁니다. 그리고 sLLM뿐 아니라 모든 LLM이 가진 고질적 문제가 앞부분과 뒷부분은 잘 기억하는데 중간을 까먹는 현상이죠. 그래서 제조사는 품질이 일정 수준 유지되는 지점을 제한 사항으로 공표하는 것입니다.

Encoder와 Decoder, 그리고 Decoder-Only

원래 오리지널 Transformer에는 Encoder와 Decoder가 둘 다 있었습니다. Encoder는 문장 전체를 한꺼번에 보고 각 단어의 의미를 추출하는 데 특화되어 있고, Decoder는 Encoder가 준 정보를 바탕으로 한 글자씩 결과를 생성하는 데 특화되어 있습니다.

그런데 연구를 하다 보니 굳이 Encoder가 필요한가 하는 의문이 들었고, Decoder만으로도 앞에 나온 단어들을 토대로 다음 단어를 맞히는 학습을 시키면 충분히 똑똑해진다는 것을 확인했습니다. 그래서 Encoder를 뜯어내고 생성에 최적화된 Decoder만 남게 된 것이죠.

Decoder-Only 모델의 가장 큰 특징은 "미래를 볼 수 없다"는 점입니다. 미래를 볼 수 있으면 학습이 안 되기 때문인데, 정답지를 보고 정답을 맞히라고 하는 것과 같은 이치입니다. 그래서 Decoder-Only 모델은 자기가 뱉은 말을 다시 입력으로 넣는 무한 루프 구조가 되었습니다.

입력으로 "오늘 점심은"이 들어가 출력으로 "돈까스"를 생성했다면, 다음 단어를 만들 때는 입력으로 "오늘 점심은 돈까스"를 넣습니다. 그렇게 "오늘 점심은 돈까스 먹자"라는 문장이 완성되는 것이죠.

Decoder-Only는 구조가 단순해서 파라미터를 수조 개로 늘려도 병렬 처리가 쉬웠고 학습 효율도 좋았습니다. 또한 '다음 단어 맞히기'라는 단순한 원리 하나로 코딩, 수학, 번역, 대화 등 언어를 쓰는 모든 일을 잘 해냈죠. 이뿐만 아니라 별도의 튜닝 없이 프롬프트만으로 문제를 푸는 능력이 매우 뛰어났기에 대세가 되었습니다.

하지만 이 방식은 딱 봐도 연산량이 어마어마할 것 같죠. 이런 엄청난 연산을 그냥 놔둘 수는 없었으니, 그래서 등장한 것이 바로 KV Cache로 중복된 계산 결과를 메모리에 저장해 두는 것입니다.

이에 대해서는 아래 링크에 잘 정리해 뒀으니 참고해 주세요.

관련 글: https://coding-review.tistory.com/613

마치며

이번 포스팅에서는 Transformer가 태동할 때부터 있었던 근본 개념들, 즉 Attention과 위치 정보 벡터, Encoder와 Decoder를 공부하고 정리해 봤습니다.

쓰면서 궁금한 것이 마구마구 쏟아졌는데, 다음 포스팅으로 뉴럴 링크와 역전파를 쓸지… 아니면 페르소나를 주입하면 왜 그대로 행동하게 되는지 프롬프트 엔지니어링의 동작 방식을 쓸지… 둘 다 쓰긴 할 거지만, 오랜만에 포스팅을 하니 쓸 내용이 너무 많네요.

일단 이번 포스팅을 통해 Transformer의 동작 방식을 깊이 있게 공부해 볼 좋은 기회가 되었습니다.

이번 포스팅은 여기서 마치겠습니다. 긴 글 읽어 주셔서 감사합니다. 오늘도 즐거운 하루 되세요!

Attention 레이어와 MLP 레이어

안녕하세요! 이번 포스팅에서는 MLP 레이어에 대해 공부한 내용을 정리해 보려고 합니다. LLM에서 MLP 레이어는 핵심 축 중에서도 핵심 축인데요. 천천히 알아보다 보면 끝에 가서는 MLP 레이어의 진수를 맛보게 되실 겁니다.

레이어들

Attention 레이어, MLP 레이어

LLM에는 크게 두 개의 레이어가 있다고 말합니다. Attention 레이어와 MLP 레이어죠. Attention 레이어는 이름 그대로 Attention 연산을 수행하는 레이어로, 단어와 단어 사이의 맥락을 파악하는 데 쓰입니다.

그에 비해 MLP 레이어는 맥락이 파악된 토큰을 입력받아, 그 토큰이 가진 정보에 '데이터'를 입힙니다. 예를 들어 "프랑스의 수도는?"이라는 맥락이 완성되면, MLP는 훈련 과정에서 학습한 "파리"라는 정보를 꺼내 옵니다.

이 때문에 트랜스포머에서는 MLP를 일종의 Key-Value Memory로 보는 시각이 있습니다. MLP는 항상 두 개의 선형 레이어가 각각 키와 값의 역할을 하기 때문이죠. 첫 번째 선형 레이어가 패턴을 감지하고, 활성화 함수를 거쳐, 두 번째 선형 레이어에서 선택된 패턴에 대응하는 실제 정보를 결괏값에 더해 줍니다.

잔차 연결

Gemma 3 모델에는 MLP 레이어가 48개 있는데, 각 레이어를 지날 때마다 "잔차 연결"을 하면서 지나갑니다. 여기서 "잔차"란 아주 조금의 차이라는 뜻으로, 그 조금의 차이만큼을 레이어마다 계속 더해 가며 의미를 확장해 나간다는 뜻입니다.

예를 들어 "자기소개 부탁해"라는 입력이 들어오면, 초반 MLP 레이어에서는 어떤 언어로 질문이 들어왔는지, 문장 구조는 어떻게 되어 있는지 같은 기본적인 분석을 마칩니다. 중간 레이어에서 자기소개를 해야 한다는 맥락을 파악하고, 마지막 레이어에서 자기소개를 준비하는 식이죠.

그래서 MLP 레이어가 많다는 것은 곧 모델의 사고가 얼마나 깊어질 수 있는가를 가늠하는 기준이 되기도 합니다. 큰 모델들이 복잡한 논리 구조나 비유를 이해하고 고차원적인 표현을 할 수 있는 이유가 여기에 있습니다.

레이어가 수십 개가 되는 순간, 아무리 성능이 좋아도 수십 번의 행렬 곱셈을 거치면서 초기 입력 정보가 희석되거나 왜곡될 수밖에 없습니다. 잔차 연결은 아무것도 하지 않아도 입력이 그대로 전달되게 해 주므로, 이런 왜곡을 막아 줍니다. 덕분에 하위 레이어의 정보가 상위 레이어까지 손실 없이 전달됩니다.

또한 잔차 연결은 딥러닝의 고질병인 기울기 소실 문제를 해결하는 핵심이기도 합니다. 역전파 시 미분값이 잔차 연결을 타고 처음부터 끝까지 끊김 없이 전달되어, 학습이 실패할 위험이 줄어들기 때문입니다. 이 덕분에 레이어가 수십, 수백 개가 되어도 모델을 안정적으로 학습시킬 수 있게 됐습니다.

인덕션 헤드와 MLP 레이어의 관계

이전에 인덕션 헤드와 잔차 스트림이라는 개념을 공부한 적이 있습니다. 자세한 내용은 아래 글을 확인해 주세요.

관련 글: https://coding-review.tistory.com/618

간략히 정리하면, 인덕션 헤드는 잔차 스트림에 올라가 있는 데이터를 보고 다음에 선택될 단어를 이전 맥락에서 찾아내어, 어떤 단어가 높은 확률로 선택되는지를 설명하는 개념입니다. 특정 레이어의 인덕션 헤드가 잔차 스트림의 정보를 읽어서 다음 단어를 스트림에 덧붙이는 과정이었죠.

이 과정 자체가 MLP 레이어에서의 잔차 연결이었던 것입니다.

트랜스포머 아키텍처를 보면 항상 `Add & Norm`이라고 적힌 부분을 찾을 수 있습니다. 여기서 Add가 바로 잔차 연결이고, Norm은 더해진 결괏값이 너무 커지거나 튀지 않도록 평균과 분산을 조정해 깔끔하게 다듬는 과정이라고 생각하시면 됩니다.

잔차 스트림과 정규화

잔차 스트림이라는 거대한 정보의 고속도로가 잘 작동하려면 두 가지 핵심 장치가 필요합니다. 무엇을 강조할지 결정하는 스위치인 "활성화 함수", 그리고 신호가 너무 커지거나 작아지지 않게 조정하는 조절기인 "정규화"입니다.

활성화 함수

활성화 함수는 "이 지식이 지금 맥락에서 중요한가?"를 판단해 다음 단계로 보낼지 말지를 결정하는 의사결정 스위치입니다.

이것이 왜 필요할까요? 활성화 함수가 없다면 아무리 레이어를 겹겹이 쌓아도 결국 하나의 거대한 선형 연산에 불과해지기 때문입니다. 활성화 함수는 계산 결과에 비선형의 성질을 주어, 복잡한 논리와 패턴을 학습할 수 있게 만들어 줍니다.

과거에는 관련이 없으면 차단하고 관련이 높으면 통과시키는 아주 간단한 스위치를 썼습니다. 시간이 지나면서 끊기지 않고 부드럽게 작동하는 방식이 더해졌고, 최근 모델들은 두 개의 연산 결과를 곱하는 방식으로 지식 추출의 효율을 극대화합니다.

정규화

정규화는 레이어를 통과하면서 들쭉날쭉해진 벡터들을 일정한 범위 안에 들어오도록 강제로 다듬는 작업입니다. 평균이 0이고 분산이 1인 정규분포를 따르도록 만들죠.

그래서 여기에 양자화를 거치면 매끄럽던 정규화 값에 노이즈가 생깁니다. 이것이 양자화된 모델의 답변 품질이 급격히 떨어지는 원인입니다.

이 내용은 아래 글과 연결됩니다.

관련 글: https://coding-review.tistory.com/607

차원의 마법

딥러닝 아키텍처에서 지식을 정교하게 분리하고 저장하는 핵심 전략은 차원을 뻥튀기했다가 다시 줄이는 것입니다. 이렇게 하면 어떤 지식이 필요하고 어떤 지식이 필요 없는지 정교하게 걸러낼 수 있습니다.

입력 벡터의 차원을 늘리는 이유는 복잡하게 얽힌 정보를 선형적으로 분리하기 위해서입니다. 쉽게 말해 복잡하게 꼬인 실타래를 2차원에서 보는 것과 3차원에서 보는 것의 차이라고 할 수 있습니다. 2차원의 실타래는 위아래가 없어 어디가 어딘지 잘 보이지 않지만, 3차원에서 보면 어느 부분을 풀어야 할지 확실하게 보이는 이치죠.

물방울 하나가 눈으로 볼 때는 별 차이 없어 보이지만 현미경으로 보면 그 안에 수많은 미생물이 살고 있는 것과도 비슷합니다.

낮은 차원에서는 데이터들이 서로 너무 가까이 붙어 있어 "사과"와 "배"를 구분하기 어렵습니다. 그런데 차원을 확 늘리면 데이터 사이에 빈 공간이 생기고, 데이터 간의 미세한 특징이 뚜렷하게 드러납니다. 그러면 활성화 함수가 어떤 정보를 고를지 결정하기 훨씬 쉬워집니다.

이렇게 차원을 늘려 놓은 상태에서 활성화 함수에 집어넣으면, 지금 맥락에서 정말 필요한 특징만 골라 뽑아낼 수 있습니다.

고차원에서 필요한 정보를 모두 추출했다면 이제 다시 원래 차원으로 줄여야 합니다. 뽑아낸 지식을 잔차 스트림과 합류시켜야 하는데, 잔차 스트림은 일정한 폭을 가지고 있기 때문입니다.

마치며

이로써 MLP의 정의와 동작 방식을 한 바퀴 쭉 돌아봤습니다. 그런데 이번 MLP 레이어는 공부를 한 건지 안 한 건지 머릿속에 잘 안 남는 개념이라, 여러 번 본 내용인데도 이해가 잘 안 되고 남에게 명확하게 설명하기가 쉽지 않네요.

시간 날 때마다 계속 보고 있는데도 머릿속에 잘 입력이 안 되는 걸 보니 수련이 더 필요할 것 같습니다. 일단 기록을 위해 블로그에 남겨 두지만, 정리가 조금 더 되면 더 쉬운 버전으로 한 번 더 써 볼 생각입니다.

그럼 여기서 포스팅을 마치겠습니다. 긴 글 읽어 주셔서 감사합니다. 오늘도 화이팅입니다!

AI 엔지니어링과 EDA의 만남

안녕하세요. 요즘 이론 공부하는 게 왜 이렇게 재밌죠… 근 5개월 정도 글을 안 쓰다가 오랜만에 쓰니까 글감이 폭발하는 것 같습니다. 이번 포스팅에서는 AI 엔지니어링을 하면서 규모가 조금만 커져도 바로 고민하게 되는 "AI 엔지니어링과 EDA"라는 주제를 다뤄 볼까 합니다.

웹 백엔드에서의 EDA

EDA는 흔히 Event Driven Architecture의 약자로 쓰이고, 메시지 브로커를 도입하면서 특정 행동을 로직에서 뜯어내고 싶을 때 주로 사용합니다. 저는 이 이름이 너무 "이벤트"에 매몰되기 쉬운 단어 선택이라고 생각합니다.

AI 엔지니어링 이야기에 앞서, 웹 백엔드 관점에서 EDA를 먼저 짚고 넘어가겠습니다.

우리가 웹 백엔드에서 EDA를, 많은 경우 카프카를 선택하게 되는 경위는 뭘까요? 어떤 문제가 생기면 EDA를 찾게 되는 걸까요?

제가 생각하는 원인은 느려터진 RDBMS의 쓰기 속도입니다. RDBMS의 쓰기를 뜯어보면 이렇습니다.

• RDBMS는 일관성과 무결성에 특화된 데이터베이스입니다.

• 쓰기 작업을 할 때 트랜잭션 간 일관성을 위해 Lock을 획득하는 과정이 있습니다. 이 락을 얻지 못하면 쓰기가 불가능합니다.

• 기본 자료구조인 B-Tree는 데이터가 자신이 누울 자리를 직접 찾아 눕는 구조입니다. 현대 RDBMS가 쓰는 B+Tree에서는 쓰기 때문에 리프 노드가 찢어지면(페이지가 분할되면) 데이터를 다시 정렬해야 합니다.

• 트랜잭션 도중 에러가 나거나 모종의 이유로 롤백해야 할 경우를 대비해 Undo 로그를 씁니다.

• 프로세스 자체가 다운됐을 때 데이터 유실을 막기 위해 Redo 로그를 씁니다. (ACID의 D에 해당하죠)

RDBMS가 이렇게 진화한 데에는 여러 이유가 있지만, 가장 큰 비중을 차지한 것은 읽기와 쓰기의 비율이라고 생각합니다.

대부분의 웹 서비스는 읽기가 압도적으로 많고 쓰기가 압도적으로 적으니, 이런 전략을 택하는 것은 당연합니다. 다만 저녁 시간의 배달의민족이나 설날·추석 시즌의 쿠팡이라면 쓰기 작업이 정말 어마어마하겠죠.

이걸 동기식으로 처리하면 쓰기가 다 끝날 때까지 기다려야 합니다. 쓰기가 진행되는 도중에 사용자가 브라우저를 내리거나 앱을 종료하면 큰 문제가 생길 것이 뻔했죠. 그래서 개발자들은 이렇게 리소스를 많이 쓰는 쓰기 작업을 로직에서 뜯어내고 싶었습니다.

그래서 등장한 것이 바로 EDA입니다.

AI 엔지니어링과 EDA

이제 AI 엔지니어링 이야기로 돌아오겠습니다. LLM을 품고 있는 컴퓨터는 쓸 수 있는 자원이 정해져 있습니다. 쿠버네티스 파드로 띄우든 온디맨드로 띄우든 마찬가지죠. 이때 가장 큰 문제는 OOM입니다. 특히 LLM은 맥락이 길어질수록 KV Cache가 엄청나게 빠르게 늘어나서, 유저가 몇 명만 붙어도 금방 OOM으로 이어질 수 있습니다.

또한 채팅 내역을 관리해야 하는 상황에서 이를 데이터베이스에 저장하면 쓰기 오버헤드를 무시할 수 없었죠. NoSQL을 쓰면 RDBMS보다는 줄일 수 있었지만, 여전히 부담스러운 것은 사실이었습니다.

이때 EDA가 등장합니다.

메시지 브로커의 큐에 작업을 넣어 두고 LLM이 처리할 수 있는 만큼만 뽑아 쓰는 버퍼가 생겼습니다. 쓰기 작업도 큐에 넣어 뒤로 미룰 수 있었으니 누이 좋고 매부 좋고, 모두가 행복해지기 시작했죠. 게다가 수평 확장의 악마답게 확장하기도 매우 편해졌습니다.

배포 관점의 장점도 있습니다. 신규 버전 B를 같은 Consumer Group으로 묶어 버리면 아무도 모르게 A/B 테스트를 할 수 있고, 이는 카나리 배포와 비슷한 맥락을 공유합니다.

마지막으로 쓰기 작업을 로직에서 뜯어냈으니 결과적 일관성(Eventual Consistency)을 얻을 수 있었고, 일관성이 그리 중요하지 않은 AI 엔지니어링에서 이 성질이 빛을 발하게 됐습니다.

EDA의 단점

EDA의 단점은 웹 백엔드에서 도입했을 때의 단점과 일맥상통합니다. 크게 보면 관리 포인트의 증가이고, 나눠 보면 다음과 같습니다.

메시지 브로커를 관리해야 한다

우선 메시지 브로커 자체를 관리해야 합니다. 카프카, RabbitMQ, Redis Stream이 바로 그것이죠. 특히 많이 쓰이는 카프카는 etcd를, 구버전이라면 ZooKeeper까지 함께 관리해야 하는데, 이런 관리 포인트의 증가는 개발자 입장에서 기술 부채가 되기 쉽습니다.

카프카 클러스터가 늘어나면 어떻게 관리할지, Consumer Group은 어떻게 관리할지, Scalability는 어떻게 할지 정말 막막해집니다.

메시지 실패 시 재처리 로직을 만들어야 한다

메시지만 실패한다면 DLQ를 직접 만들어 완화할 수 있습니다. 그런데 이게 트랜잭션과 엮이기 시작하면 꽤나 골치 아파집니다.

메시지는 정상적으로 갔다 → 그런데 백엔드에서 에러가 났다 → 쓰기 작업이 롤백됐다

그럼 재시도해야죠. 그런데 어떻게 할까요?

메시지는 정상적으로 갔으니 카프카 입장에서는 DLQ에 저장할 필요도 없었던, 정상 처리된 메시지입니다. 이걸 어떻게 다시 보낼까요?

결국 Outbox Pattern이니 뭐니 하면서 또 추가 개발이 들어갑니다. 이것도 상당한 리스크죠.

관련 글: https://coding-review.tistory.com/566

LLM의 맥락을 유지해야 한다

LLM의 맥락을 유지하는 데는 LangChain의 자체 메모리 저장도 도움이 되지만, 긴 맥락을 유지하려면 결국 Redis 같은 경량 인메모리 데이터베이스가 필요하기 마련입니다.

그럼 Redis가 죽으면 어떡하죠? 죽게 놔둘 수는 없으니 또 HA 기술인 Sentinel이니 Cluster니 이것저것 덕지덕지 바르게 됩니다. 그 과정에서 생기는 문제는 덤이고요.

관련 글: https://coding-review.tistory.com/542

따라서…

결국 AI 엔지니어링의 끝은 웹 백엔드와 닿아 있는 것 같습니다. 얼마나 안정적으로, 적은 컴퓨팅 자원으로, 응답 속도까지 고려하면서 잘 해낼 수 있는지가 주된 관건이 되겠지요.

요즘 드는 생각이지만, AI 엔지니어링이라고 거창하게 이름을 붙였을 뿐 사실 웹 백엔드에 관리 포인트가 하나 늘어난 기분입니다. 쿠버네티스가 그랬고 카프카가 그랬고 Istio가 그랬고 데이터베이스가 그렇죠. 다 하나같이 어마어마하게 큰 기술들이고, 감당하기 힘들어지는 순간 독립 팀으로 찢어지는 것들입니다. 이제 AI 엔지니어링도 그렇게 되지 않을까 싶습니다.

늘 그렇듯 오버엔지니어링과 좋은 기술 선택 사이에서 줄타기를 잘 하면서, 안정적으로 개발하고 운영해야겠습니다.

이번 포스팅도 읽어 주셔서 감사합니다. 오늘도 즐거운 하루 되세요!

특명: 추론 속도를 올리기 위한 온몸 비틀기 (Speculative Decoding)

이번 포스팅에서는 AI 엔지니어링에서 추론 속도를 끌어올리기 위한 서커스 기술, 추측 디코딩(Speculative Decoding)을 정리해 보겠습니다.

AI 엔지니어링을 하다 보면 자연스럽게 빠져드는 것이 TPS를 높이기 위한 여러 노력인데, 거기서 단연 빠지지 않고 등장하는 개념이 바로 추측 디코딩입니다. 개념과 함께, 실전에서 쓸 때 주의해야 할 점까지 짚어 보겠습니다.

Speculative Decoding

Transformer 기반 LLM은 반복적인 문맥 확인을 위해 문장 전체를 훑으면서(사실 전체는 아닙니다. `n_ctx`에 해당하는 토큰만큼 확인합니다) 한 단어씩 내뱉습니다. 추측 디코딩은 여기서 생기는 문제를 해결하기 위한 기술로, 한 단어를 검수하나 열 단어를 검수하나 별 차이가 없는 GPU의 성질을 적극 활용해 추론 속도를 끌어올리는 방법론입니다.

기본 아이디어는 이렇습니다. LLM 모델이 미리 생성된 여러 단어를 보고, 그 단어가 원래 자기가 뱉으려던 단어와 일치하면 통과시키고 그렇지 않으면 수정합니다. 답변 전체의 품질은 그대로 유지하면서 속도만 끌어올리는 것이죠.

그럼 그 단어들은 조상님이 만들어 주나요?

한 번에 빠르게 만드는 데 특화된 작은 모델(드래프트 모델)이 단어를 여러 개 한꺼번에 뱉고, 그걸 큰 모델(타깃 모델)이 빠르게 확인하는 구조입니다. 이런 장점 덕분에 Apple Silicon 진영, Windows 진영, NVIDIA 진영 모두 추측 디코딩을 활발하게 지원하고 있습니다.

추측 디코딩을 적용하면 이론적으로 TPS가 2~3배 가까이 개선되고, 조금 극단적인 환경(모든 조건이 최상으로 맞춰진 경우)에서는 4배까지도 올라갑니다.

여기서 짚고 넘어가야 할 점이 있습니다. 바로 "답변의 품질이 그대로 유지된다"는 부분인데요. 정말일까요?

Rejection Sampling

LLM이 등장하기 훨씬 전부터 있던 개념으로, 단순하게 말하면 수학적 트릭입니다. 확률 분포에 일치하는 것은 수용하고 일치하지 않는 것은 거절하는 로직인데, 이 개념이 AI 엔지니어링에도 적용된 것이죠.

그래서 추론 속도가 중요한 서비스에서 주로 사용합니다. 국내 사례로는 네이버 클로바의 클로바노트가 음성을 텍스트로 옮기는 데 이 기술을 활용해 톡톡히 재미를 본 것으로 보입니다.

실전에서 쓸 때 유의사항

꽤나 흥미로운 기술이지만, 실전에서 신경 써야 할 부분이 제법 있습니다. 다만 지금부터 나열할 유의사항은 VRAM이 넉넉한 환경에서는 전혀 해당하지 않습니다. 한정된 VRAM으로 개발하던 제 발버둥이라고 생각해 주시면 됩니다.

1. 타깃 모델과 드래프트 모델의 토크나이저를 맞출 것

첫 번째로, 큰 모델과 작은 모델의 토크나이저가 일치해야 답변 품질이 깨지지 않습니다. 토크나이저가 일치한다는 것은 두 모델이 같은 확률 분포 곡선의 형태를 띠고 있어야 한다는 뜻입니다. 타깃 모델로 LLaMA를 썼다면 드래프트 모델도 LLaMA를 써야 한다는 이야기죠.

가장 간단하면서도, 지키지 않으면 답변 품질에 치명적인 결함이 생기는 가장 중요한 유의사항입니다.

2. 양자화 정도도 맞출 것

두 번째는 타깃 모델과 드래프트 모델의 양자화 정도가 일치해야 한다는 점입니다. 타깃 모델은 4비트인데 드래프트 모델은 8비트라면 답변 품질에 결함이 생깁니다.

양자화는 확률 분포에 노이즈를 만드는 일이고, 부동소수점 이하 부분을 평평하게 눌러 버리는 것입니다. 그래서 서로 양자화 정도가 맞지 않으면 타깃 모델이 드래프트 모델의 답변을 수락할 확률이 떨어지고, 결국 TPS 향상도 크게 얻을 수 없습니다.

3. TPS를 극단적으로 올리려면 드래프트 모델은 최대한 작게

많은 개발자가 권장하는 타깃 모델과 드래프트 모델의 비율은 100:1입니다. 100B짜리 타깃 모델을 쓴다면 드래프트 모델은 1B를 쓰라는 이야기죠.

드래프트 모델의 역할은 답변 품질을 올리는 것이 아니라 얼마나 빠르게 드래프트 토큰을 만들어 내느냐입니다. 그래서 두 모델의 격차가 크면 클수록 TPS는 더 극단적으로 향상됩니다.

제 경우 타깃 모델이 12B, 드래프트 모델이 1B였습니다. 권장 비율에 한참 못 미치다 보니 그렇게 큰 성능 향상은 얻지 못했습니다.

4. 웬만하면 NVIDIA 환경에서, 그리고 서버에서 쓸 것

Apple Silicon 진영과 Windows에서 쓰는 llama.cpp는 vLLM과 달리, 타깃 모델과 드래프트 모델을 오가면서 생기는 컨텍스트 스위칭 비용이 꽤 크게 작용합니다. 여기서 오는 문제도 크지만, 개인용 PC와 서버의 하드웨어 차이도 함께 봐야 합니다.

개인 PC는 메모리 대역폭이 아무리 좋아 봐야 200GB/s인 반면, 서버용 하드웨어는 일반적으로 3,000GB/s에 달합니다. 추측 디코딩은 메모리에서 가중치를 읽어 오는 횟수가 늘어나는 작업이라, 대역폭이 낮은 환경에서는 소프트웨어 최적화보다 하드웨어의 대역폭 한계가 TPS를 결정합니다.

5. VRAM이 부족하면 쓰지 말 것

어찌 보면 당연한 이야기입니다. LLM을 단독으로 쓸 때도 모델과 KV Cache를 VRAM에 올려야 하는데, 드래프트 모델까지 올리면 메모리 압박이 심해져 성능 향상을 기대하기 어렵습니다.

vLLM은 Paged Attention으로 메모리 단편화를 어느 정도 막을 수 있지만, llama.cpp를 쓰던 저는 단편화가 심하게 생겨서 VRAM이 조금만 부족해져도 바로 OOM으로 뻗는 일이 잦았습니다.

그렇다고 VRAM 점유를 줄이려고 KV Cache를 양자화하는 선택은 더 큰 병목으로 이어집니다. KV Cache를 양자화하면 다시 Dequantize하는 과정에서 심각한 오버헤드가 생겨, 원하던 TPS가 안 나올 수도 있거든요.

결론

"VRAM이 빵빵한 NVIDIA 그래픽카드 서버가 아니라면 쓰기가 조금 꺼려진다."

제 경우 타깃 모델로 `Gemma-3-12b-it-Q8_0`을, 드래프트 모델로 `Gemma-3-1b-it-Q8_0`을 썼습니다. 기존 9TPS가 11TPS가 되면서 아주 조금은 향상됐지만, 그에 따르는 VRAM 부족이나 간헐적으로 컨텍스트가 길어질 때 생기는 OOM이 너무 치명적이어서 추측 디코딩은 포기했습니다.

제 맥북도 VRAM을 24GB나 쓸 수 있어서 개인용 컴퓨터 중에서는 좋은 축에 속하지만, VRAM은 정말 항상 부족하네요. (나도 H200 줘라 ㅜㅜ)

대안으로 Medusa나 Eagle도 찾아봤습니다. 그런데 이것들도 결국 NVIDIA 환경, 정확히는 vLLM 환경에 최적화되어 있어서, llama.cpp를 쓰는 저로서는 타깃 모델 하나만 쓰는 게 속 편한 상황이 됐습니다.

이리저리 온몸 비틀기를 해 봤지만 결국 상처만 남아 버렸죠.

그래도 의미 없는 경험은 아니었다고 생각합니다. 덕분에 추측 디코딩도 더 공부할 수 있었고 llama.cpp에도 더 익숙해졌으니까요. 저도 얼른 NVIDIA 서버를 다뤄 보고 싶네요. 아직 vLLM이 미지의 영역이라, 한번 꼭 H100이나 H200에서 개발해 보고 싶습니다.

오늘 포스팅은 여기서 마치겠습니다. 긴 글 읽어 주셔서 감사합니다. 오늘도 즐거운 하루 되세요!

우린 전사 직원이 AI 다 써야돼요. (GPU 서버 한대를 쥐어주며..)

안녕하세요! 오랜만에 써 보고 싶은 주제가 생각나서 컴퓨터 앞에 앉았습니다. 이번에는 실제로 있을 법한 상황을 재구성해서, 엔지니어링으로 문제를 풀어 가는 과정을 써 볼까 합니다.

요즘은 엉덩이 무거운 금융권까지 AI를 도입하겠다고 나서는 분위기입니다. 국내 기준으로 개발자 수요는 전반적으로 줄었지만, AI 엔지니어만은 수요가 폭발하고 있죠.

그런데 AI 엔지니어링, 그 끝이 결국 백엔드와 닿아 있다는 걸 깨닫는 데는 그리 오래 걸리지 않았습니다.

상황은 이렇습니다. 고객이 AI를 도입하겠다면서 GPU 서버는 한 대만 주겠다고 합니다. 이때 우리가 쓸 수 있는 해결책을 하나씩 따져 보겠습니다.

sLLM을 올리는 것 자체는 문제가 아닙니다. 문제는 사용자마다 늘어나는 KV Cache죠. 쓸 만한 질답이 오가려면 토큰이 16k 정도는 필요하고, 멀티턴까지 고려하면 더 필요할 수도 있습니다. 요즘 GPU 서버 한 대에 올리기 가장 좋은 모델로는 Gemma 4가 소문이 자자하죠.

그럼 Gemma 4의 32k가 점유하는 KV Cache는 얼마일까요? 15GB 정도 됩니다. 요즘은 양자화 기술이 좋아져서 웬만하면 8비트로 양자화해 버리지만, 사용자가 전 직원이라면 그래도 많이 부족합니다.

case 1. 메시지 큐잉을 도입하는 경우

가장 먼저 떠올릴 수 있고, 가장 쉽게 풀리는 방법입니다.

대규모 서비스에서 EDA와 AI 엔지니어링은 궁합이 좋습니다. EDA가 지금껏 성행하는 이유도 중간 버퍼로 서버 부하를 막고 메시지를 안정적으로 도달시키는 데 특화돼 있기 때문이죠. RDB는 커넥션 풀이 정해져 있는, "이만큼만 요청을 받고 싶습니다"의 대표주자라 EDA와 잘 맞았습니다.

LLM도 마찬가지입니다. 받을 수 있는 요청의 양이 정해져 있으니까요. 실제로 대규모 서비스에서는 EDA와 LLM을 많이 붙여 씁니다.

#### side effect 1. 유지보수

관리 포인트가 늘어나는 건 어쩔 수 없습니다. 다만 Kafka나 Redis는 유지보수가 꽤 까다로워서, "그냥 하나 추가하죠" 하고 가볍게 넣을 수 있는 물건은 아닙니다.

#### side effect 2. 단일 장애 지점

카프카가 죽으면? 레디스가 죽으면? 이 질문에 대한 답이 미리 정해져 있어야 합니다. 답이 없으면 MQ가 죽는 순간 GPU 서버도 OOM으로 같이 죽어 버리거든요.

case 2. 우린 서버 많은데요?

"우리 솔루션은 Active-Standby로 들어가서 서버 많아요~"

그렇다면 HA 솔루션을 기대해 볼 만합니다. Kafka Cluster나 Redis Sentinel 같은 것들이죠. 이것도 물론 방법이 됩니다.

#### side effect 1. 관리 포인트는 어떡하지

하나도 관리하기 쉽지 않은데 여러 개를 하겠다면, 말리지는 않겠습니다만 각오는 하셔야 합니다. 큰 힘에는 큰 책임이 따르는 법이니까요.

#### side effect 2. 디버깅은 어떡하지

요청을 비동기로 미루면 디버깅이 지옥이 된다는 건 잘 아실 겁니다. 그런데 그마저도 세 갈래로 나뉘어 있으니, 어디서 요청을 보냈는지 어디서 문제가 생겼는지 알기가 쉽지 않죠.

case 3. 코드로 해결

사실 이 얘기를 하고 싶었습니다. 이 글을 쓰게 된 계기이기도 하고요. 들어가기 전에 제 얘기를 조금 하겠습니다.

저는 문제를 늘 구조적으로 푸는 걸 좋아했고, 또 잘했습니다. 위에서처럼 아키텍처로 풀어 나가는 것도 좋았고, 코드 안에서 전략 패턴·어댑터 패턴·프록시 패턴 같은 디자인 패턴으로 푸는 것도 좋아했습니다. 요즘은 AI에게 이렇게 만들어 달라 저렇게 만들어 달라 하면 뚝딱 나오니 더 좋았고요.

그러다 보니 제 약점이 점점 드러났습니다. 바로 알고리즘입니다.

GPU 서버가 한 대뿐이라 부하를 막아야 한다 → 메시지 큐 도입 → 단일 장애 지점은? → HA로 해결

메시지 큐를 도입하는 이유가 관리 포인트를 줄이는 것이라면서 레디스를 쓰자고 했으니, 말 다 했죠.

더 쉽고 좋은 해결책이 알고리즘에 있었습니다. 바로 Bounded Queue와 Token Bucket입니다.

Bounded Queue는 큐의 크기를 무한히 두지 않습니다. 1,000개면 1,000개로 정해 놓고, 다 차면 429 Too Many Requests를 반환해 클라이언트가 재시도하게 합니다. 다만 이 재시도를 지수 백오프로 흩어 주지 않으면, 큐에 자리가 나는 순간 대기하던 클라이언트가 한꺼번에 요청을 쏟아 서버를 죽일 수 있습니다. 이게 그 유명한 Thundering Herd죠.

Token Bucket은 1초에 발급할 수 있는 토큰 수를 제한하고, 요청이 그 토큰을 물어야만 들어올 수 있게 만듭니다. 제한을 넘은 토큰은 버려집니다. 그러면 들어오는 요청이 예측 가능해지고, 따라서 GPU 서버의 자원 사용량도 예측할 수 있습니다. 수용량을 제한할 수 있게 되는 겁니다.

이 둘을 쓰면 카프카도 레디스도 없이 문제를 풀 수 있다는 장점이 생깁니다.

마치며

당분간 제 공부 방향은 이쪽이 될 것 같습니다. 아키텍처 관점의 문제 해결은 몇 년 동안 해 왔으니, 이제 방향을 조금 틀어 보려 합니다. 지금까지 아키텍처로 풀던 문제들을 알고리즘으로 풀어 보려고요.

물론 둘을 같이 쓰면 가장 좋지만, 한쪽 테크트리를 탔으니 반대쪽도 한번 타 보겠습니다.

포스팅은 여기서 마무리하겠습니다. 긴 글은 아니었지만 봐 주셔서 감사합니다. 오늘도 즐거운 하루 되세요!

LLM한테 페르소나를 적용하면 어떻게 이를 수행하는걸까?

지금 사람들에게 가장 널리 알려진 프롬프트 엔지니어링은 아마 "페르소나"를 부여하는 것일 겁니다. LLM에게 페르소나를 주면 찰떡같이 잘 반응하는데, 내부적으로는 어떤 동작이 일어나기에 그 페르소나가 계속 유지되는 걸까요?

요즘은 프롬프트 엔지니어링이 유형별로 잘 정립되어 있어서 우리 같은 일반인도 충분히 적용할 수 있는 범주에 들어왔습니다. 그래서 사실 "엔지니어링"이라고 거창하게 부를 만한가 싶기도 합니다.

다만 이게 초창기에 왜 엔지니어링의 영역에 들어왔는지는 이번에 공부하면서 알게 됐습니다. 프롬프트 엔지니어링의 기반에는 Transformer 알고리즘에 대한 이해가 필요했고, 그래서 초기 단계에서는 엔지니어링이라 부를 만한 무언가였던 것이죠.

이제 저와 함께 Transformer 알고리즘을 복습하면서, 페르소나를 프롬프트로 입력하면 어떤 과정을 거쳐 페르소나가 입혀지는지 알아보겠습니다.

들어가기 전에, 제가 정리한 Transformer 알고리즘에 대한 기반 지식이 필요합니다. 아래 링크에 잘 정리해 뒀으니 한번 확인하고 오시길 추천드립니다.

관련 글: https://coding-review.tistory.com/614

이번 포스팅에서는 총 세 가지 관점에서 페르소나를 분석하고, 끝으로 성능 병목을 확인해 볼 예정입니다.

• LLM에서 페르소나가 적용되는 과정 — Attention 레이어 / In-Context Learning / Masked Self-Attention

• 페르소나와 KV Cache, 그리고 성능 병목

Attention 레이어, In-Context Learning, Masked Self-Attention은 모두 LLM의 특성입니다. 이것이 페르소나와 만났을 때 어떤 시너지를 내는지 살펴보고, 마지막으로 페르소나가 KV Cache에 미치는 영향과 그것이 어떻게 병목으로 이어지는지 다루겠습니다.

이제 본격적으로 들어가 보시죠!

LLM에서 페르소나가 적용되는 과정

Attention 레이어와 페르소나

LLM에는 기본적으로 레이어라는 것이 있습니다. 모델이 얼마나 깊이 있게 생각할 수 있는지를 나타내는 단계로 이해해도 무방한데요. 임베딩 모델로 유명한 BERT는 12개, Llama-3-8b는 32개의 레이어를 가지고 있습니다. 제가 개인적으로 뛰어나다고 생각하는 Gemma-3-27b-it은 62개의 레이어가 있죠.

파라미터 수가 늘어나면 레이어도 선형적으로 늘어난다고 보시면 됩니다. 즉,

"레이어가 겹겹이 많을수록 모델이 더 깊이 있는 생각을 할 수 있다"

이렇게 이해하셔도 무방합니다.

이제 LLM에게 "너는 까칠한 개발자야"라고 프롬프팅했다고 가정해 봅시다. 모델은 이 프롬프트를 앞에서부터 읽겠죠. 이 토큰이 Attention 레이어의 첫 부분을 통과하면 모델은 "아, 나는 개발자구나" 정도로 이해합니다. 그리고 N번째 레이어를 모두 통과하고 나면 "아, 나는 자바를 싫어하고 파이썬을 좋아하는, 츤데레 말투를 써야 하는 개발자구나"처럼 훨씬 복잡하고 구체적인 특징으로 변합니다.

조금 더 공학적으로 풀어 보겠습니다.

"너는 까칠한 개발자야. 이 코드 분석해 줘."라는 프롬프트를 넣으면 Positional Encoding이 각 토큰에 위치 정보 벡터를 부여합니다. 편의상 페르소나를 0번째, 뒤따르는 요청을 1번째라고 하죠.

맨 처음 토큰이 Attention 레이어를 통과할 때 가장 먼저 들어오면 가중치가 전파되고, 첫 토큰을 생성해 답변할 때의 Attention Map에서 가장 강한 자극을 받게 됩니다.

다음 토큰 생성에 가장 큰 영향을 미치는 Attention Score의 첫 연산은 쿼리와 키의 내적입니다. 질문에 해당하는 쿼리와 인덱스에 해당하는 키가 서로 강하게 연결되어 있으니, 모델은 이 페르소나로 대답하도록 강한 드라이브가 걸리는 것이죠.

조금 다른 이야기이지만, 프롬프트 엔지니어링에서 페르소나는 맨 앞에, 가장 중요한 지시는 맨 뒤에 넣으라고 하는 이유를 두 가지 관점에서 볼 수 있습니다.

첫째, LLM의 고질적인 문제 관점. 이미 여러 논문으로 밝혀진 문제 중 하나가 "모델은 맨 앞과 맨 뒤는 잘 기억하지만 중간은 잘 기억하지 못한다"는 것입니다. 그러니 맨 앞에 페르소나를, 맨 뒤에 가장 강한 제약을 두면 이 둘만큼은 확실히 수행하게 되는 것이죠.

둘째, RLHF 관점. 요즘 LLM은 RLHF라는 방법을 주로 씁니다. 모델이 인터넷의 데이터를 싹 학습한 뒤 답변을 생성하면, 사람이 "이 답변은 사람 같군, 이 답변은 너무 기계 같아"라고 검수하는 강화학습 과정입니다.

이 과정에서 사람이 문맥을 보고 답변을 정해 주기 때문에, 모델은 사람이 정답이라 판단한 값에 더 큰 가중치를 둡니다. 그런데 인간은 통상 핵심 로직을 마지막에 배치하죠. 그 대화 습관이 그대로 모델에 이식되는 겁니다.

그래서 모델은 마지막에 오는 내용이 중요하다고 판단하게 됩니다.

In-Context Learning과 페르소나

LLM은 인터넷의 수많은 텍스트를 학습하면서, 특정 문맥 뒤에는 어떤 말투와 어떤 지식이 뒤따르는지에 대한 통계적 패턴을 익힙니다.

프롬프트에 페르소나를 넣는 행위는 모델의 거대한 파라미터 공간 중에서 그 페르소나와 일치하는 영역으로 확률 분포를 강제로 이동시키는 것입니다. 그래서 인터넷에서 학습한 패턴을 그대로 잘 따라갈 수 있는 것이죠.

Masked Self-Attention과 페르소나

우리가 흔히 쓰는 LLM은 Decoder-Only인 경우가 많습니다. Decoder-Only 모델은 앞에서 생성한 토큰을 다음 입력으로 집어넣는 자기 회귀 구조를 띠고 있습니다. 앞서 만든 토큰을 계속 입력으로 받기 때문에 페르소나가 그대로 유지되는 것이죠.

페르소나와 KV Cache, 그리고 성능 병목

방금 말했듯 Decoder-Only는 자기 회귀 구조라 같은 연산을 계속 반복해야 하는 문제가 있습니다. 이를 KV Cache에 담아 두어 연산을 빠르게 합니다. KV Cache라고 부르는 이유는, 이미 행렬곱에서 연산한 K와 V 벡터는 변하지 않기 때문에 키와 값을 메모리에 저장해도 전혀 문제가 없기 때문입니다.

KV Cache가 점유하는 메모리를 구하는 공식은 다음과 같습니다.

2 × Hidden Layer × Head Dimension × KV Head × n_ctx × B

여기서 다른 건 볼 필요 없고 n_ctx만 보면 됩니다. n_ctx는 LLM에게 주어지는 입력 토큰, 또는 LLM이 기억하고 있는 토큰의 수라고 이해하시면 됩니다.

즉 프롬프트가 길어질수록 n_ctx가 커지고, 이는 KV Cache의 크기를 선형적으로 늘리는 원인이 됩니다. 그러면 추론을 시작하기도 전에 VRAM을 선점하게 되므로, 첫 토큰이 나오기까지의 시간인 TTFT가 길어집니다.

TTFT뿐만이 아닙니다. 개별 세션이 차지하는 KV Cache가 커지면 하나의 GPU에서 동시에 처리할 수 있는 배치 크기가 줄어들고, 결국 전체 처리량도 줄어듭니다.

그래서 프롬프트가 너무 비대해지지 않도록 주의를 기울여야 성능 병목이 생기지 않습니다.

물론 동적으로 변하지 않는 프롬프트는 미리 연산해 둠으로써 TTFT를 줄이는 방법이 있지만, 이건 실전에 관한 내용이라 여기서는 더 언급하지 않겠습니다.

마치며

이번 포스팅은 개인적으로 궁금했던 내용을 정리해 봤는데, Transformer의 기본 동작 원리를 이해하고 나서 공부하니 훨씬 수월했습니다. 그래서 꼭 Transformer의 기본 동작 원리를 먼저 보고 오시길 추천드립니다.

GPT가 등장한 지 만으로 3년 정도, AI 엔지니어링이라는 것이 등장한 지 만으로 2년 정도 됐습니다. 그래서 Transformer라는 기본 개념 안에서 계속 돌고 도는 느낌이 들기도 합니다만, 그렇기에 Transformer에 대한 이해가 그 어느 때보다 중요해진 시점인 것 같습니다.

다음 포스팅에서는 뉴럴 링크와 역전파를 공부해 정리해 볼 생각입니다. 이번 포스팅은 여기서 마치겠습니다. 긴 글 읽어 주셔서 감사합니다. 오늘도 즐거운 하루 되세요!

특명: 학습하지 않은 패턴을 학습해라 (Few Shot의 동작 방식)

안녕하세요! 거의 3주 만에 블로그 포스팅으로 돌아왔습니다. 요즘 글 쓰는 게 뜸해졌는데, 딱히 바쁘지도 않았던 것 같은데 공부할 틈이 안 나서 이제야 글을 쓰네요.

오늘은 few shot(이하 퓨샷)의 동작 원리를 공부하고 정리해 보겠습니다. 주변 개발자분들은 이런저런 기술도 써 보시고 요즘 핫하다는 에이전트 개발에 힘쓰고 계시던데, 저는 그런 것보다 동작 원리 같은 게 더 궁금한지 모르겠네요.

그런 의미로 이번 포스팅은 다음 순서로 이어집니다.

• 퓨샷이란?

• 퓨샷의 발견

• 퓨샷의 동작 원리

• 퓨샷의 증명

첫 챕터에서는 퓨샷이 무엇인지 가볍게 이야기하고, 두 번째 챕터부터 퓨샷이 어떻게 탄생했는지를 다룹니다. 세 번째 챕터에서 동작 원리를 파악하고, 마지막으로 그 원리가 어떻게 퓨샷으로 이어지는지 증명까지 살펴본 뒤 마치겠습니다.

그럼 본격적으로 시작해 보죠!

Few Shot이란?

프롬프트 엔지니어링에서 말하는 퓨샷은 LLM에게 답지를 제공하는 것입니다. 이를 통해 LLM이 미처 학습하지 못한 부분에서 패턴을 새로 익히게 하거나, 엔지니어링 관점에서는 할루시네이션을 줄이기도 합니다.

AI 엔지니어링에서 말하는 RAG도 큰 틀에서 보면 퓨샷의 범주에 들어갑니다. 요즘은 순수한 RAG가 아닌 구조화된 RAG를 쓰면서 "RAG is Dead"라고들 하지만, 예전에 개발자들이 RAG에 열광한 이유는 LLM이 헛소리하는 빈도를 획기적으로 줄여 준다는 데 큰 의미가 있었기 때문입니다.

퓨샷은 특정 인물의 말투를 따라 해 달라고 하거나 특정 포맷으로 답변하게 하는 식으로, LLM이 학습하지 않은 패턴을 익히게 해 문맥을 주입합니다. 꽤나 다양한 방면으로 쓰이죠.

그런데 이거 좀 이상하지 않나요? 어떻게 LLM이 학습하지 않은 영역에서, 내가 제시한 패턴을 이렇게 기가 막히게 따라 할까요?

Few Shot의 발견

퓨샷은 페르소나 주입과 어느 정도 비슷한 맥락을 가집니다. 특정 패턴대로 움직이도록 주입한다는 점이 그렇죠. 다만 페르소나가 "너는 20년 차 베테랑 개발자야"처럼 일반적인 상황을 주입하는 것이라면, 퓨샷은 특정 캐릭터의 말투를 흉내 내게 하거나 특정 포맷(예: JSON)으로 답변을 요구할 때 주로 씁니다.

발현되는 방식도 다릅니다. 페르소나 주입은 Attention 레이어를 처음 통과하는 프롬프트 앞부분이 다음 답변의 방향을 강제하면서 높은 가중치를 얻는 방식입니다. 반면 퓨샷은 Attention 레이어가 이전 토큰과 다음 토큰의 연관 관계를 이용해 패턴을 주입하는 방식으로 발현됩니다.

기계론적 해석학

GPT-2가 한창 개발 중이던 시절, OpenAI에서 안전한 AI를 만들자는 일념으로 모인 연구원들이 AI 블랙박스를 연구하기 위해 '해석 가능성' 팀을 만들었습니다. 기계론적 해석학의 출발은 정말 순수했는데요. "AI가 어떻게 동작하는지도 모르는데 우리가 어떻게 안전한 AI를 만들겠다는 말이냐"라는 한 연구원의 작은 꿈에서 시작했습니다.

2019년 OpenAI가 마이크로소프트로부터 10억 달러라는 어마어마한 투자를 받으면서, 내부 연구원들의 반응은 둘로 갈렸습니다.

"비영리 기업이라며! 안전한 AI를 만들자며!" vs "그래도 회사가 유지되려면 투자를 받아야지!"

이에 뜻이 맞지 않던 일부 개발자들이 Anthropic을 세우고 Claude라는 AI를 만들기 시작했습니다. Claude의 슬로건은 "안전한 AI, 통제 가능한 AI를 만들자"였죠. 처음의 뜻을 가지고 모였던 연구원들이 다시금 AI 블랙박스를 연구하기 시작했고, 그것이 "기계론적 해석학"의 탄생이었습니다.

퓨샷의 발견

기계론적 해석학이라는 단어를 만들고 연구를 진두지휘하던 Anthropic의 연구원 '크리스 올라'가 2020년 12월과 이듬해 3월에 두 편의 논문을 발표합니다.

관련 논문: https://transformer-circuits.pub/2021/framework/index.html

관련 논문: https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html

첫 번째 논문은 트랜스포머 알고리즘을 수학적 회로로 분해한, 이 분야의 '창세기' 같은 논문이 되었습니다. Attention 레이어들이 단순히 순차적으로 연산되는 것이 아니라 '잔차 스트림'이라는 거대한 공용 메모리 공간에서 각자의 결괏값을 독립적으로 읽고 쓴다는 것을 증명했죠.

또한 모델의 가중치 행렬인 Q, K, V가 단순한 행렬 곱셈 덩어리가 아니라 각각 고유한 로직을 수행하는 모듈임을 시각화해, AI 블랙박스를 화이트박스로 바꿔 보는 시야를 제공했습니다.

이어 두 번째 논문에서는 LLM이 프롬프트의 문맥을 파악하고 '퓨샷 러닝'을 수행하는 근본 원리를 밝혀냈습니다. 제목에서 알 수 있듯 당시 연구원들은 퓨샷이라는 단어 대신 'In-Context Learning'이라는 말을 썼습니다. 말 그대로 '문맥 학습'이라는 단어를 만들어 낸 것이죠.

다음 내용부터는 트랜스포머 알고리즘과 Attention 레이어에 대한 이해가 선행되어야 합니다. 아래 링크를 꼭 참고하고 오세요. 그러지 않으면 용어 설명이나 맥락이 급진전한 것처럼 느껴질 수 있습니다.

관련 글: https://coding-review.tistory.com/614

Few Shot의 동작 원리

이 챕터에서는 수학적인 내용은 최대한 배제하고(저도 수학은 몰라서…) 공학적인 의미로 최대한 직관적으로 풀어 보겠습니다.

Attention Layer 분해

연구원들은 우선 Attention 레이어의 앞부분에 집중했습니다. 0층·1층·2층 레이어만 남기고 MLP 레이어를 제거한, Attention 전용 '장난감 모델'부터 분석했죠. 이때 다음과 같은 요소들을 발견했습니다.

• 잔차 스트림(Residual Stream) : 트랜스포머의 각 계층이 정보를 읽고 쓰는 선형적인 통신 채널입니다.

• QK 회로 : 어느 위치의 토큰에 주의를 기울여 정보를 가져올지 정하는 회로입니다.

• OV 회로 : 주의를 기울인 토큰에서 어떤 정보를 가져와 출력에 반영할지 결정하는 회로입니다.

• 인덕션 헤드 : 2층 레이어에서 나타나는 헤드입니다. 과거 문맥에서 현재 토큰과 동일한 토큰을 찾은 뒤 그다음에 왔던 토큰을 예측하고 복사하는 패턴을 보이며, 퓨샷을 설명하는 가장 결정적인 요소입니다.

개요는 이 정도만 하고 본격적으로 동작 원리를 정리해 보겠습니다.

QK 회로와 OV 회로

일반적으로 Attention 메커니즘은 Query, Key, Value로 설명하지만, 연구원들은 이를 두 개의 독립적인 저차원 행렬로 결합해 설명했습니다.

• QK 회로는 Query와 Key가 동작하는 회로로, "어디서 정보를 가져올 것인가?"에 답합니다. 정보의 '이동 경로'를 결정하죠. 네트워크 통신에 비유하면, 현재 토큰이 과거 문맥 속 어떤 토큰에게 정보를 요청할지 IP 주소를 찾는 과정과 같습니다.

• OV 회로는 Output과 Value가 동작하는 회로로, "어떤 정보를 가져와 어떻게 출력할 것인가?"에 답합니다. 이동할 정보의 '내용'을 결정하죠. QK 회로가 통신 채널을 열어 주면, OV 회로는 그 채널로 어떤 데이터를 보내 최종 예측값을 바꿀지 정합니다.

이 두 회로가 결합되면 1층 트랜스포머 모델의 각 Attention 헤드는 일종의 거대한 통계적 룩업 테이블처럼 작동합니다. 예를 들어 `keep in mind`처럼, `keep`이라는 단어를 봤고 현재 단어가 `in`이라면 다음 단어로 `mind`를 출력하도록 하는 식이죠.

K-결합

0층과 1층 레이어에서 각 회로가 생성되고 결합하면서, 2층 레이어에서 K와 V가 서로 결합해 퓨샷의 전체 그림이 완성됩니다.

2층의 인덕션 헤드는 1층에서 '결합'이라는 현상이 일어나면서, 이전 층의 출력이 다음 층의 입력으로 이어져 훨씬 복잡한 알고리즘을 구현합니다.

K-결합의 공학적 의미는 이렇습니다. 토큰 A, B, C가 있다고 가정하면(예: `keep in mind`), 현재 토큰 B에 이전 토큰 A의 정보가 덮어씌워져 있다는 것입니다.

조금 더 풀면, 현재 토큰이 A일 때 QK 회로는 과거의 A를 찾는 게 아니라 과거의 A 바로 다음에 있던 B의 위치를 정확히 찾아내어 그 정보를 복사해 올 수 있다는 의미입니다.

쉬운 예시로 보겠습니다. 이전에 '해리 포터'라는 단어가 있었고 현재 토큰이 '해리'를 출력하려 한다면, 이전의 '해리'를 보는 게 아니라 이미 그 '해리'와 연결된 '포터'를 바라보고 있습니다. 그래서 '해리'를 출력함과 동시에 '포터'를 출력할 수밖에 없다는 뜻이죠.

즉 '해리 포터'가 한 번쯤 등장했다면, 다음에 '해리'를 출력할 때 '포터'가 줄줄이 사탕처럼 따라 나오게 됩니다. "해리가 등장하면 다음은 포터일 확률이 높아!"라는 패턴을 인식하게 되는 것입니다.

V-결합

1층 Attention 헤드의 출력이 2층 Attention 헤드로 얽혀 들어가는 현상이 V-결합입니다. 수학적으로는 Attention 패턴 행렬끼리, OV 행렬끼리 곱해지는 것이고요.

공학적으로 보면, 두 개의 Attention 헤드가 마치 컨베이어 벨트처럼 정보를 두 번 이동시킵니다. 물리적으로는 두 개의 헤드이지만 수학적으로는 완전히 새로운 기능을 하는 하나의 '가상 헤드'가 탄생하는 셈이죠. 그래서 V-결합을 '가상 어텐션 헤드'라고 부르기도 합니다.

쉽게 예를 들면, "현재 문장의 시작 부분으로 이동해!"라고 가리키는 헤드와 "주어로 이동해!"라는 헤드 두 개가 V-결합을 하면, "이전 문장의 주어를 찾아 그 정보를 현재로 가져와라"라는 고차원적인 문법적 추론을 수행할 수 있게 됩니다.

K-결합이 이전 토큰의 패턴을 읽어 다음 토큰을 예측하는 데 쓰인다면, V-결합은 헤드들이 서로 정보를 주고받으며 단일 헤드로는 불가능한, 복잡하고 먼 거리의 정보 처리를 가능하게 만드는 메커니즘입니다.

Few Shot의 증명

여기까지가 퓨샷의 기본 골자입니다. 그런데 이것만으로는 인덕션 헤드가 퓨샷의 증명이 될 수 없었습니다. 이건 이론일 뿐이고, 실제로 적용해 증명이 되어야 비로소 퓨샷의 동작 원리라고 말할 수 있으니까요. 그래서 연구원들은 재밌는 가설을 세웠습니다.

인덕션 헤드가 모델 크기와 상관없이 2층 레이어에서 발현된다는 것은 이미 발견한 뒤였습니다. 그리고 0층 레이어에는 인덕션 헤드가 없으므로, 일반적인 0층 레이어만으로는 모델이 패턴을 파악하지 못한다는 사실도 알고 있었죠.

"그럼 인덕션 헤드만 똑 떼어 0층 레이어에 가져다 놓으면 어떻게 될까?"

궁금했던 연구원들은 실험을 진행했고, 놀라운 결과를 얻었습니다. 0층 레이어만 있는 단순한 모델에서 패턴을 학습하는 현상이 나타난 것입니다. 이로써 연구원들은 '인덕션 헤드'가 Few Shot Learning을 일으키는 주된 요소라고 판단하고, 이를 논문에 담았습니다.

마치며

이번 포스팅에서는 퓨샷을 깊이 있게 공부하고 정리하는 시간을 가졌습니다. 기계론적 해석학이라니, 정말 이름부터 가슴 뛰게 하는 무언가가 있네요. 어릴 때 늘 기계의 동작 원리가 궁금했던 제가, 커서는 AI의 동작 원리 공부에 눈을 떠 재밌게 공부하고 있습니다.

최근 아는 개발자분들과 작게 모임을 가졌는데, 2~3년 정도 저를 옆에서 봐 온 분들이 "너는 진짜 연구원 재질이다"라고 하시더군요. 속으로 '그렇게 별난가…?' 싶어 조금 어리둥절했습니다.

여러분 중에도 기계론적 해석학에 관심 있으시다면 '크리스 올라'의 블로그를 적극 추천드립니다!

오늘은 여기서 마무리하고 다음 포스팅으로 찾아뵙겠습니다. 오늘 하루도 고생하셨습니다. 다음 글에서 뵈어요!

AI가 나아가야하는 방향은 어디일까?

안녕하세요. 이번에 "에세이"라는 카테고리를 새로 만들었습니다. 지금까지 AI 엔지니어링 포스팅은 동작 원리나 내부 구조를 이해하는 "이론" 파트와, 엔지니어링에서 고려해야 할 것들이나 아키텍처 설계를 다루는 "실습" 파트로 운영해 왔습니다. 이번 글은 처음엔 이론 공부로 시작했는데 정리하다 보니 이론도 실습도 아닌 칼럼처럼 되어 버려서, 이렇게 에세이라는 카테고리를 새로 열게 되었습니다.

에세이 카테고리는 AI에 대한 칼럼 한 편 본다고 생각하고 편하게 보셔도 됩니다. 기술적인 이야기는 최대한 줄이고, AI 석박들이나 개발자들이 이야기하는 AI 현상에 대해 제 생각을 풀어 놓는 자리로 운영할 것 같습니다.

그럼 본격적으로 시작해 보죠!

LLM이 추상적인 개념을 표현하는 방법

LLM은 단어의 속성을 "벡터"라는 형식으로 이해하고 답변을 생성합니다. 요즘 LLM 모델들은 수만 개의 차원으로 이루어진 벡터를 씁니다. 예를 들어 왕과 여왕은 "성별"만 다른 속성으로 인지하죠. 그런데 이렇게 단순한 것 말고 "슬픔" 같은 추상적인 개념은 어디에 저장될까요?

우리가 흔히 말하는 Embedding은 단순히 단어를 숫자로 바꾼 것이 아니라, 수천·수만 개의 차원에 점을 찍는 행위입니다. 3차원 공간에서는 가깝다/멀다만 중요하지만, 1만 개가 넘는 차원에서는 서로 다른 "방향"이 존재할 수 있습니다.

특정 차원 하나가 "슬픔"을 담당하는 것이 아닙니다. 수만 개의 차원이 아주 복잡하게 조합된 **특정한 방향 벡터 자체**가 "슬픔"이라는 개념을 가리키게 되는 것이죠.

효율적인 압축의 비밀

LLM은 자신이 가진 뉴런 수보다 더 많은 개념을 저장해야 합니다. 그래서 뉴런 하나가 오직 하나의 의미에만 반응하는 대신, 여러 개의 뉴런을 동시에 활성화시킵니다. '강아지'라는 단어에 '강아지' 뉴런만 반응하는 게 아니라 '복슬복슬한', '충성심' 같은 뉴런들이 함께 반응하는 식이죠.

거대한 데이터를 해시 함수로 압축해 저장하듯, LLM도 수많은 개념을 고차원 공간에 최대한 겹치지 않게 쑤셔 넣습니다. 다만 너무 많이 겹쳐 놓으면 모델이 헷갈려하는데, 이걸 잘 분리해서 처리하는 능력이 곧 모델의 성능과 직결됩니다.

cf) LLM의 뉴런에 대응되는 개념은 뭘까? 진짜 뉴런은 아닐 텐데

LLM에서 말하는 뉴런은 모델의 각 레이어에 존재하는 개별 가중치 유닛입니다. 특정 입력이 들어왔을 때 "이 개념이 얼마나 포함되어 있는가?"를 계산해서, 신호를 보낼지 말지를 결정하죠.

그리고 수만 개의 뉴런이 동시에 활성화된 '전체적인 패턴'이 바로 벡터입니다. 1번 뉴런이 0.1, 2번 뉴런이 0.8, 3번 뉴런이 0.3… 이렇게 활성화된 수치를 한 줄로 세우면 그게 그 시점의 '개념 벡터'가 됩니다.

이걸 왜 뉴런이라고 부르게 됐을까요? 인간의 뇌에서도 '사과'를 담당하는 뇌세포가 따로 있는 게 아니라, 사과를 볼 때 뇌 전체의 수많은 뉴런이 '특정한 패턴'으로 반짝입니다. LLM의 벡터가 바로 이 방식을 그대로 모방한 것이기 때문입니다.

얀 르쿤의 LLM 회의론

AGI라는 단어가 주는 마법

우리는 AGI라는 단어에 홀린 듯이 반응합니다. 이제는 인간의 지능을 넘어선 무언가로 뭉뚱그려 부르는 것처럼 보이죠. 하지만 OpenAI의 창립자이자 수석 과학자로서 GPT의 전성기를 이끌었던 일리야 수츠케버는 다르게 말합니다. **우리는 이미 AGI에 도달했고, 이제 도달해야 할 것은 AGI가 아니라 "초지능"이라는 것입니다.**

왜 이런 이야기가 나올까요? AGI라는 단어가 처음 생기던 시절의 AI로 거슬러 올라가면 이해하기 쉽습니다. 그때의 AI는 특정 문제를 잘 푸는 형태로 발전했습니다. 체스를 두는 AI, 바둑을 두는 AI, 포커를 치는 AI… 이때 많은 연구원이 꿈꾸던 것이 바로 "인간처럼 일반적인 지능을 가진 AI를 만들자"였습니다.

그래서 연구원들은 그런 AI를 일반 인공지능, 즉 AGI라고 불렀습니다. 그렇다면 지금의 GPT나 Gemini, Claude처럼 인간과 대화가 가능한 모델은 이미 그 정의를 충족한다는 것이 일리야 수츠케버의 입장이죠.

물론 모든 개발자와 연구원이 이를 받아들이는 것은 아니고, AGI를 초지능과 같은 것으로 여기는 경향도 여전히 남아 있습니다. 일반인들도 대개 초지능을 AGI라고 생각하고 있어서, 굳이 단어를 교정해 줘야 하나 싶기는 합니다.

아무튼 앞에서 LLM이 추상적인 개념을 어떻게 저장하는지 이야기했는데, 이것이 얼핏 인간 뇌의 방식과 많이 닮아 있어 많은 연구원이 놀라워했고 실제로 몇몇 문제를 해결하기도 했습니다.

특히 개발 쪽에서는 '코딩'이라는 노동이 점점 사라지고 있습니다. 이제는 더 고차원적인 문제를 풀거나 비즈니스 관점에서 문제를 해결하는 쪽으로 산업이 고도화되고 있죠.

LLM 회의론

다만 AI 4대 석박이라 불리는, 메타의 AI 수석 엔지니어이자 뉴욕대 교수인 얀 르쿤은 LLM의 한계를 지적합니다. 현재의 LLM 방식으로는 결코 인간 수준의 지능에 도달할 수 없다고 못을 박는, 아주 강경한 사람이죠. 인간 뇌의 뉴런과 비슷하게 동작한다는 LLM에 왜 한계가 있다고 보는 걸까요?

그가 가장 강조하는 것은 현재의 LLM에 "세계 모델"이 없다는 점입니다. 인간은 텍스트가 아니라 물리적 상호작용으로 세상을 배우는 반면, LLM은 글로 세상을 배웁니다. **글로 수영을 배우는 것과 같다**는 것이죠. 그렇게 학습된 LLM은 인간이 이해하는 방식으로는 결코 이해할 수 없다는 것이 그의 입장입니다.

LLM은 텍스트 데이터 사이의 통계적 상관관계만 학습합니다. 그래서 "컵을 놓치면 바닥으로 떨어진다"라는 문장은 알지만, "중력"이라는 물리적 실체를 느껴 본 적은 없습니다.

그래서 얀 르쿤은 언론이 즐겨 쓰는 "박사급 지능"이라는 표현에 강하게 반대합니다. 실제 박사는 어떤 물리 현상을 두고 다음 실험을 어떻게 이어 갈지 "판단"을 내립니다. LLM에는 그 판단이 없으니, 엄청나게 거대한 계산기나 백과사전일 뿐이라는 겁니다.

또 하나. LLM은 단어를 하나씩 내뱉고 그 결괏값을 다시 입력으로 넣어 다음 단어를 만듭니다. 반면 인간은 말을 꺼내기 전에 결론을 먼저 내리고 그 목표를 향해 문장을 구사하는 "계획"을 하죠. 계획이나 추론 과정 없이 통계적으로 다음 단어를 찍어내는 방식으로는 복잡한 문제를 단계별로 풀어내는 지능을 가질 수 없다는 것이 그의 주장입니다.

그 대안으로 얀 르쿤은 JEPA라는 새로운 구조를 제안했습니다. JEPA는 단어를 예측하는 대신 "세상의 구조를 예측하는" 모델입니다. 이미지나 영상의 다음 프레임을 픽셀 단위로 맞추는 것이 아니라, 그 안에 담긴 "개념"과 "움직임"을 예측하는 방식이죠. 이렇게 하면 AI가 인간처럼 물리 법칙을 이해하고, 목표를 이루기 위해 행동을 "계획"할 수 있게 된다는 것이 그의 핵심 아이디어입니다.

마치며

이런 AI가 언제쯤 등장할까요? 등장하기만 한다면 어마어마한 관심을 받게 될 것은 분명합니다. 지금의 GPT니 Gemini니 하는 것들이 다 의미가 없어질 수도 있고요. 이런 격변의 시대에 살고 있다는 게 가슴 뛰고 재밌습니다.

컴퓨터와 인터넷이 어떻게 변했는지 겪었던 부모님 세대와, AI가 어떻게 변하는지 겪고 있는 우리 세대가 얼추 비슷해 보이더군요. 앞으로가 정말 흥미진진합니다. 정말 모든 직업이 사라질까요? 아니면 인간이 밥그릇을 지키려고 기술 발전의 발목을 잡을까요?

일리야 수츠케버의 말을 인용하며 이번 포스팅을 마무리하겠습니다.

"지금 AI와 관련된 법안이 매끄럽게 진행되지 않는 건, 우리가 아직 '실수하지 않는 AI'를 본 적이 없기 때문입니다. 당신은 실수하지 않는 AI를 생각해 보신 적 있나요? 만약 그런 AI가 등장한다면, 그때 본격적으로 시작될 겁니다."

- 일리야 수츠케버 -