지난 주말 아이들 낮잠 자는 동안 정리한 글이에요. 작년 가을부터 시작했다가 9개월 흐지부지 끝낸 사이드들이 책상 위에 자료로 쌓여 있는 거 보고 “이번엔 진짜 끝까지 가보자”고 마음먹고 6주 굴린 게 Lumen이라는 사이드예요. Rust로 LLM 추론 컴파일러를 처음부터 짠 거고, v0.5.0에서 release tag 박고 maintenance mode 선언으로 일단락 지었거든요. 6주짜리 사이드라 GitHub repo는 정리됐는데, 막상 보니까 “처음 공부하는 분이 들어오면 어디부터 봐야 하나” 싶더라구요.
그래서 시리즈로 10편짜리 튜토리얼을 GitHub markdown으로 정리해 뒀는데, 그건 진짜 “공식 문서” 톤이라 좀 딱딱해요. 블로그엔 친구한테 설명하듯 풀어볼게요. 1편은 큰 그림부터.
저도 사이드 시작하기 전엔 ChatGPT가 답을 만드는 게 막연히 “어떤 신비한 계산”이라고만 생각했거든요. 막상 추론 엔진을 직접 짜보니까 그게 진짜 단순한 그림으로 정리되더라구요. 이번 편에서 그 그림을 잡으면 다음 편부터 나오는 컴파일러·JIT·양자화·SIMD가 왜 필요한지 자연스럽게 따라옵니다. 이 그림이 진짜 핵심이에요. 이거 하나 잡으면 나머지는 디테일 채우기.

LLM이라는 게 뭐 하려고 만든 건가
먼저 ChatGPT, Claude, Gemini 같은 LLM이 무엇을 하려고 만든 건지 짧게 정리할게요. 사실 LLM은 인간 두뇌의 일부 동작을 통계적으로 흉내 낸 것이에요. 이게 인공지능 책 첫 장에 나오는 얘긴데, 막상 코드로 짜보면 진짜 그 흉내가 어떤 모양인지 명확해져요.
두뇌에서 정보가 처리될 때 한 뉴런이 다음 뉴런에게 신호를 보내요. 그 신호의 세기가 시냅스 강도(synaptic weight)라는 값으로 정해져 있고, 학습 과정에서 그 강도가 조금씩 조정되면서 두뇌가 패턴을 익혀 갑니다. 어느 시냅스가 강해지고 어느 시냅스가 약해지는지가 두뇌의 “지식”이에요. 어린아이가 한국어를 배운다는 건 한국어 패턴에 맞는 시냅스 강도 조합을 익혀 가는 거고, 외국어를 배운다는 건 그 외국어 패턴에 맞는 새 시냅스 강도 조합을 만들어 가는 거예요.
LLM은 이걸 거대한 행렬(숫자 표)로 모방합니다. 두뇌의 뉴런 대신 숫자 벡터, 시냅스 강도 대신 행렬 안의 숫자 값을 써요. 학습 과정에서 이 숫자 값들이 조정돼서 인간 언어의 패턴을 통계적으로 압축합니다. 답을 만들 때는 이 숫자 표를 한 번 통과시키면 “다음 단어로 가장 그럴듯한 것”이 점수로 나와요.
여기까지가 LLM이라는 개념의 정체성이에요. 솔직히 이 정도면 인공지능이 무서운 마법이라고 생각했던 게 좀 허탈해질 정도예요. 거대한 곱셈표 한 장을 통과시키는 일이거든요. 다만 그 곱셈표가 인간 언어의 통계 패턴을 어떻게든 압축해서 담고 있는 거고, 그래서 통과 결과가 “이해한 것처럼” 보이는 거죠.
제가 사이드 시작했을 때 가장 먼저 의심한 게 이거였어요. “정말 곱셈표 한 장 통과시키는 게 답이 되는 거 맞아?” 근데 직접 짜보니까 진짜 그래요. 거짓말 아니에요. 24개 동(layer)이라는 시설을 차례로 통과시키는 일이고, 그 결과의 점수 분포에서 가장 높은 값 골라내는 게 끝이에요.
이제 이 곱셈표를 컴퓨터가 어떻게 실제로 실행하는지가 진짜 주제고, 여기부터는 도시 건축 비유로 풀어 갑니다. 두뇌가 무엇을 흉내 내려는지는 위 단락으로 충분하고, 그 흉내가 컴퓨터 안에서 어떻게 구현되는지는 도시 비유가 훨씬 명확하더라구요.
학습과 추론은 다른 일
풀어가기 전에 한 가지 짚고 갈게요. LLM 다루는 두 가지 작업이 있는데, 그게 학습(training)과 추론(inference)이에요. 이 둘이 진짜 다른 작업이에요.
학습은 곱셈표 안의 숫자 값들을 만들어 가는 작업이에요. 데이터(인터넷 글, 책, 코드 등)를 보고 “이 단어 다음에 저 단어가 나올 확률” 패턴을 통계적으로 누적해서 숫자 값들을 조정해요. 학습은 어마어마하게 비싸요. ChatGPT 같은 큰 모델은 학습에만 GPU 수만 장과 몇 달 시간이 들고 전기료가 수십억 원. 일반 개발자가 손댈 영역이 아닙니다.
추론은 이미 만들어진 곱셈표를 통과시켜서 답을 뽑는 작업이에요. 학습은 끝났다고 치고, “이 곱셈표를 가지고 다음 단어 1개 결정하자”가 추론. 추론은 학습보다 천 배 정도 싸요. CPU 한 대로도 0.5B 같은 작은 모델은 충분히 돌릴 수 있어요.
이 글에서 다루는 건 추론이에요. 학습은 손도 안 댈 거예요. 이미 만들어진 670MB 곱셈표를 받아서 그걸 어떻게 통과시킬지가 주제예요. 추론도 어려운데 학습까지 다루면 진짜 끝도 없거든요. 사이드로 시작한 거니까 일단 추론만 집중.
실제로 LLM 추론 엔진은 학습 엔진과 완전히 다른 코드베이스예요. llama.cpp(추론), vLLM(추론) 같은 도구들이 학습 도구(PyTorch, JAX)와 따로 만들어진 이유가 그거예요. 작업 양상이 너무 달라서.
모델 670MB의 정체 – 거대한 도시 청사진
제가 Lumen 사이드에서 쓰는 모델은 Qwen2.5-0.5B-Q8_0이라는 한국어 LLM이에요. 알리바바 클라우드가 학습시켜서 공개한 작은 한국어 친화 모델. 파일 크기 보면 정확히 670MB입니다. 처음 다운받았을 때 “이게 다 뭐길래 670MB나 되지” 싶었거든요.
까 보면 진짜 단순해요. 670MB의 거의 전부가 숫자예요. 정확히는 약 6억 3천만 개의 8비트 정수와 약간의 설정값. 그게 다입니다. 코드도 아니고 그림도 아니고 그냥 숫자 6.3억 개. 8비트 정수는 -128부터 127까지의 정수 한 개씩이고, 그게 6.3억 개 모이면 6.3억 바이트 = 약 630MB. 나머지 40MB는 설정 메타데이터.
이걸 도시 비유로 풀면 670MB는 거대한 도시 전체의 청사진이에요. 그것도 좀 특이한 형태의 도시인데, 24개 동(棟)이 순서대로 연결된 한 직선 도시입니다. 1동에서 2동, 2동에서 3동, …, 23동에서 24동까지 일방향. 각 동은 입구로 정보가 들어와서 출구로 다음 동에 전달되는 구조예요. 디즈니랜드처럼 돌고 도는 게 아니고 직선이에요.
각 동(transformer layer)의 청사진은 비슷합니다. 한 동 안에 5개의 큰 처리 시설이 있고, 그 시설들의 입출력 관계가 정해져 있어요. 24개 동이 같은 형태의 청사진을 공유하지만 시설 안의 숫자(가중치)는 동마다 다릅니다. 1동의 시설 A와 2동의 시설 A는 모양은 같지만 내용물은 달라요.
670MB 청사진 = 24개 동 × (5개 큰 시설 + 작은 부수 시설)
한 동의 구조:
입구 → 시설 A (QKV 매트멀)
시설 B (어텐션)
시설 C (출력 매트멀)
시설 D (게이트·업 매트멀)
시설 E (다운 매트멀)
→ 출구 → 다음 동의 입구시설 5개의 정확한 크기는 다음과 같아요. 한 동(layer) 기준입니다.
한 동의 시설별 크기:
qkv (Q + K + V 합쳐서): 896 × 2560 = 약 2.3MB
wo (출력): 896 × 896 = 약 0.8MB
gate: 896 × 4864 = 약 4.3MB
up: 896 × 4864 = 약 4.3MB
down: 4864 × 896 = 약 4.3MB
─────────────────────────────────────
한 동 합계: 약 16MB
24개 동 합계: 16MB × 24 = 약 384MB
+ 토큰 임베딩 (어휘 사전): 약 130MB
+ 출력 헤드: 약 130MB
+ 메타데이터: 약 26MB
─────────────────────────────────────
전체: 약 670MB이 숫자들을 외울 필요는 없어요. 중요한 건 두 가지예요. 첫째, 한 동의 크기가 약 16MB이고 그게 24개 쌓여 있다. 둘째, 한 동 안에서 가장 큰 시설은 gate·up·down(각 4.3MB)이다. 이게 6편 양자화에서 “왜 weight를 8비트로 압축하는 게 그렇게 중요한가”의 정량적 근거예요.
컴퓨터는 단어를 다루지 않더라구요
조금 더 구체적으로 풀게요. 저도 이번에 정리하면서 새로 알게 된 건데, 컴퓨터는 단어를 직접 다루지 않아요. 단어를 숫자 벡터로 바꿔서 다룹니다.
예를 들어 “안녕”이라는 단어는 컴퓨터 안에서 약 896개의 실수 배열로 표현돼요. [0.12, -0.34, 0.78, ...] 같은 모양인 거죠. 이걸 임베딩(embedding) 벡터라고 부릅니다. 토큰 임베딩 시설에 어휘 사전 통째로 들어 있어요. “안녕”이라는 토큰을 받으면 그 토큰에 해당하는 896개 숫자를 꺼내주는 거예요.
896이라는 숫자는 Qwen2.5-0.5B 모델의 설정값이에요. 다른 모델은 다른 값을 써요. Qwen2.5-7B는 3584, Llama-3-8B는 4096. 이 숫자가 클수록 모델 “이해 폭”이 넓어진다고 (이론적으로) 보지만, 그만큼 매트멀 크기도 커져서 추론이 느려져요.
이 896개 실수 배열이 도시 입구에서 1동으로 들어갑니다. 1동의 시설 5개를 차례로 통과하면서 값이 바뀌어 다른 896개 실수 배열로 변환돼요. 그게 2동의 입구로 전달됩니다. 24동까지 통과하면 마지막에 어휘 사전 전체 크기(약 15만 단어)의 점수 배열이 나와요. 점수가 가장 높은 단어가 “다음 단어”인 거예요.
"안녕" → [0.12, -0.34, 0.78, ...] (896개 실수)
→ 1동 통과 → 새 896개
→ 2동 통과 → 새 896개
→ ...
→ 24동 통과 → 새 896개
→ 최종 변환 → 15만 단어 점수 배열
→ 가장 높은 점수: "하세요"이게 LLM이 답 하나를 만드는 전체 과정입니다. 진짜 이게 다예요. 신기하지 않나요? 저는 처음 이걸 측정 데이터로 직접 확인했을 때 좀 허탈했어요. “이게 다라고?” 싶었거든요. 무슨 마법 같은 거 있는 게 아니라 그냥 거대한 곱셈표 통과 24번 + 점수 최대값 1개 골라내기.
참고로 단어라는 표현을 계속 썼는데, 정확히는 토큰이에요. 한 단어일 수도, 단어의 일부일 수도, 글자 한 개일 수도 있는 단위. 한국어 “안녕하세요”는 보통 1~3개 토큰으로 쪼개져요. 영어 “Hello”는 1개 토큰, “Hellooo”는 2~3개. 일본어나 중국어는 또 다른 패턴. 토큰이 정확한 단위지만 직관적으로 “단어”가 편하니까 이 글에서는 그냥 단어로 부를게요.
그리고 토큰을 어떻게 쪼개는지(토크나이저)도 별도 시설이에요. 이것도 모델에 포함돼 있어요. 한국어 잘하는 모델은 한국어 토크나이저가 잘 짜여 있고, 영어 중심 모델은 한국어 토크나이저가 비효율적이라 같은 문장이 더 많은 토큰으로 쪼개져요. 추론 속도에 영향이 큽니다.
답 만들기 = 도시 한 번 통과해서 단어 1개 결정 반복
여기서 첫 번째 중요한 사실. LLM은 답 전체를 한 번에 만들지 않습니다. 단어 하나를 만들고, 그 단어를 입력에 붙여서 다시 처음부터 시작하고, 또 다음 단어를 만들고, 또 붙이고. 이걸 답이 끝날 때까지 반복해요.
저도 처음엔 “어떻게 한 번에 긴 답을 뱉지?” 싶었는데, 알고 보니 그게 아니더라구요. 한 단어씩 만드는 걸 반복하는 거였어요. ChatGPT가 답할 때 글자가 한 글자씩(정확히는 한 토큰씩) 흐르듯 나오잖아요. 그게 진짜 그 속도로 만들어지고 있는 거예요. 한 번에 다 만들어 놓고 천천히 보여주는 게 아니라.
도시 비유로 풀면 이래요. 도시를 한 번 통과시켰더니 “하세요”가 답으로 나왔다고 합시다. 그러면 “하세요”를 입력 끝에 붙여서 도시 통과를 처음부터 다시 합니다. 이번엔 입력이 “안녕하세요”가 돼요. 또 통과해서 “,”가 나오고. 또 통과해서 “저”가 나오고.
입력: "안녕"
1회: "안녕" → 도시 24동 통과 → "하세요" (답: "안녕하세요")
2회: "안녕하세요" → 도시 24동 통과 → "," (답: "안녕하세요,")
3회: "안녕하세요," → 도시 24동 통과 → " 저" (답: "안녕하세요, 저")
4회: "안녕하세요, 저" → 도시 24동 통과 → "는" (답: "안녕하세요, 저는")
5회: "안녕하세요, 저는" → 도시 24동 통과 → " 레추" (답: "안녕하세요, 저는 레추")
...문장 하나 만들려면 도시를 수십 번 통과합니다. 32 단어짜리 답이라면 도시를 32번 통과한다는 뜻이에요.
여기서 두 번째 중요한 사실. 매번 도시를 통과할 때 670MB짜리 청사진을 처음부터 끝까지 다시 읽습니다. 같은 청사진을 32번 다시 읽는 거예요. 이게 LLM 추론이 느린 가장 큰 이유 중 하나입니다.
이게 진짜 이상해 보일 수 있어요. “한 번 본 청사진을 왜 또 봐?” 저도 처음 알았을 때 정말 비효율적이라고 생각했어요. 근데 이게 LLM 추론의 본질이라서, 일단 그렇다고 받아들이고 다음 절에서 왜 그래야 하는지 설명할게요.
참고로 일부 정보는 한 번 계산한 걸 재사용해요. 그게 7편에서 나올 KV 캐시예요. 입력의 앞부분(이미 본 단어들)의 attention 중간값은 다음 통과 때 재사용 가능. 다만 청사진(weight) 자체는 매번 다시 읽어야 해요. KV 캐시는 임시 중간값이고, weight는 영구 저장된 곱셈표거든요.
왜 청사진을 매번 다시 읽나 – 자재 운송이 진짜 병목
당연한 의문 들죠. “한 번 읽은 청사진을 그대로 보관해 두면 안 되나요?”
저도 처음엔 그렇게 생각했어요. 근데 도시 비유로 풀면 답이 명확해요. 청사진은 자재 창고(RAM)에 보관돼요. 그런데 청사진을 본 공사 현장(CPU)이 청사진 전체를 한 번에 외울 수 없어요. 공사 현장의 작업대(CPU 캐시 메모리)는 작아서 청사진 일부만 펼쳐 둘 수 있거든요.
제 PC 기준으로 보면 이래요. RAM이 32GB. CPU 캐시는 L1 32KB + L2 512KB + L3 32MB. 670MB 청사진은 L3에도 안 들어가요. L3 캐시가 32MB니까 청사진의 5%만 들어가요. 한 동(16MB)도 통째로는 안 들어가고 절반밖에. 그래서 청사진을 통과시키려면 자재 창고에서 작업대로 청사진을 조금씩 가져와야 해요.
자재 창고(RAM)와 공사 현장(CPU) 사이에는 도로(메모리 버스)가 있어요. 이 도로에는 처리량 한계가 있습니다. 컴퓨터로 환산하면 RAM에서 CPU로 1초에 약 50~80GB 정도만 가져올 수 있어요. (제 PC AMD Ryzen 9 7950X 기준 실측 약 80GB/s, 노트북은 30~50GB/s)
670MB 청사진을 한 번 통과 = 청사진 한 번 다 읽기 = 약 0.008초
670MB × 32회 통과 = 21.4GB 자재 운송 = 약 0.27초이 0.27초는 자재(데이터)를 도로(메모리 버스)로 운반하는 데만 들어가는 시간이에요. 실제 공사(계산) 시간은 따로입니다.
이게 LLM 추론의 본질이에요. 계산 자체가 느린 게 아니라, 자재를 가져오는 게 느려요. 이 문장이 시리즈 후반부의 거의 모든 주제를 관통합니다. 5편에서 다룰 SIMD, 6편 양자화, 8편 멀티스레딩 다 이 한 줄에서 출발해요.
이거 알고 나서 진짜 허탈했거든요. CPU가 빠른 게 중요한 게 아니라 메모리 도로가 좁은 게 문제였다니. 미리 알았으면 좋았을 텐데 싶었어요. 사이드 처음 시작할 때 SIMD 명령어 인코더 짜는 데 일주일 썼는데, 그게 효과를 보려면 메모리 병목이 어떻게든 줄어야 한다는 걸 한참 뒤에 깨달았거든요.
메모리 도로 폭의 의미
“1초에 80GB”가 얼마나 좁은 건지 감이 안 올 수 있어요. 비교해 볼게요.
이론적 최대 곱셈 속도 (AMD Ryzen 9 7950X 16코어, AVX-512):
→ 약 2조 번/초 (2 TFLOPS, fp32 기준)
메모리에서 데이터 가져오는 속도:
→ 약 80GB/초 = 약 200억 fp32 숫자/초 (fp32 1개 = 4바이트)
비율: 곱셈 능력이 메모리 운반 능력의 약 100배곱셈 능력이 메모리 운반 능력의 100배예요. 다시 말해 CPU는 자재 100개 받으면 100번 곱셈하고 다음 자재 100개 기다리는 식. 자재 1개 받으면 1번 곱셈하고 그 다음 자재가 도착할 때까지 99 cycle 놀아요. CPU가 일하는 시간보다 노는 시간이 훨씬 많은 거예요.
이걸 측정-주도 의사결정 관점에서 한 번 더 풀어볼게요. 만약에 SIMD를 도입해서 한 번에 8개씩 곱한다고 해도, 자재가 한 번에 1개씩만 도착하면 8배 가속이 안 나와요. 자재 운송이 1배 그대로니까. 그래서 LLM 추론에서는 “한 번 가져온 자재를 최대한 많이 활용하기“가 핵심 전략이에요. 5편 SIMD, 6편 양자화, 8편 멀티스레딩이 다 이 전략의 변주예요.
한 동 안에는 뭐가 있나 – 매트멀 5개
이제 도시 한 번 통과할 때 정확히 무슨 일이 일어나는지 더 자세히 봅니다.

도시 안 24개 동(layer)이 각각 5개 큰 시설로 나뉜다고 했죠. 한 동의 구조를 자세히 보면 이래요. 위에서 한 번 봤지만 이번엔 처리 순서까지 같이 봅니다.
한 동(layer) 통과 순서:
1. 입구 정비 (RMSNorm) — 작음
2. QKV 시설 (3개 결합표) — 큼 (매트멀)
3. 위치 표시판 부착 (RoPE) — 작음
4. 어텐션 시설 (상호 참조) — 중간
5. 출력 시설 (wo 결합표) — 큼 (매트멀)
6. 합치기 (residual + RMSNorm) — 작음
7. 게이트·업 시설 (gate·up 결합표) — 큼 (매트멀)
8. 비선형 변환 (SiLU + 곱셈) — 작음
9. 다운 시설 (down 결합표) — 큼 (매트멀)
10. 합치기 — 작음각 시설은 일종의 자재 결합표예요. “입력 자재 A개와 시설 안 수치(weight)를 조합해서 출력 자재 B개를 만든다”는 규칙. 이 자재 결합 계산을 컴퓨터 용어로 매트멀(matrix multiplication, 행렬 곱셈)이라고 합니다.
한 동을 통과하는 게 매트멀 5번. 24개 동 통과면 매트멀 120번. 단어 32개를 만들려면 매트멀 3840번이에요.
이 중에서 매트멀(2, 5, 7, 9 단계)이 시간의 약 95%를 잡아먹어요. 나머지 작은 처리들은 다 합쳐서 5%. 그래서 LLM 추론을 빠르게 한다는 건 거의 매트멀을 빠르게 한다는 말과 같습니다.
이게 사이드 시작하기 전엔 잘 와닿지 않았는데, 측정해 보니까 정말 95%가 매트멀이더라구요. RMSNorm이니 RoPE니 하는 거 다 합쳐도 5%. 그래서 컴파일러 짤 때도 매트멀 가속에 거의 모든 노력을 쏟게 됐어요.
실제 측정 시간 분배
Lumen v0.5.0에서 측정한 시간 분배를 잠깐 보여드릴게요. 단어 32개 만들기 기준입니다.
한 단어 만들기 (decode, 1 thread 기준 약 22ms):
매트멀 (4단계 × 24동 = 96번 호출): 약 21ms (95%)
├── qkv 매트멀: 약 5.5ms
├── wo 매트멀: 약 2.0ms
├── gate+up 매트멀: 약 6.5ms
└── down 매트멀: 약 7.0ms
attention (24동): 약 0.7ms (3%)
RMSNorm + RoPE + 부수: 약 0.3ms (2%)
─────────────────────────────────────
전체: 약 22ms매트멀 4종(qkv, wo, gate+up, down) 중에서도 gate+up과 down이 좀 더 시간을 잡아먹어요. 시설 크기 비교(앞에서 본 4.3MB × 3)와 정확히 일치해요. 큰 시설이 시간도 많이 먹는 거. 당연한 얘긴데 측정으로 확인해 보면 좀 안심돼요.
매트멀이 정확히 어떤 계산인가
매트멀은 학교에서 배운 행렬 곱셈이에요. 두 표를 입력으로 받아서 새 표를 출력하는 계산. 도시 비유로 풀면 “공사 현장에서 입력 자재 묶음과 시설 내부의 결합 규칙을 가지고 출력 자재 묶음을 만드는 일”이에요.
입력 자재 묶음 A: [896개 실수] (1줄짜리, decode 모드)
시설 결합 규칙 B: [896 × 9728] (896줄, 9728칸)
출력 자재 묶음 C: [9728개 실수] (1줄짜리)
계산 규칙:
C[k] = sum_{i} A[i] × B[i, k] (k = 0..9727)풀어 쓰면 이래요. 출력의 k번째 칸은 입력 전체와 시설 규칙 B의 k번째 열을 “하나하나 곱해서 다 더한 값”이에요. 한 칸을 만드는 데 곱셈 896번 + 덧셈 895번. 출력이 9728칸이면 한 매트멀당 곱셈 약 870만 번입니다.
24동 × 매트멀 5개 × 평균 870만 번 ≈ 한 단어 만드는 데 곱셈 약 10억 번. 32 단어 답을 만들려면 320억 번이에요.
이거 숫자만 보면 무서워요. “320억 번 곱셈?” 근데 현대 CPU는 그 정도 가능해요. AMD Ryzen 9 7950X 같은 CPU가 1초에 약 2조 번 곱셈 가능(2 TFLOPS, AVX-512 기준). 320억 번이라면 이론적으론 0.016초면 됩니다. 16ms.
근데 우리가 실제로 측정한 결과는 32단어 만드는 데 약 700ms가 걸렸어요(Lumen v0.5.0 1 thread). 이론치(16ms)의 약 44배. 8 thread 기준 약 480ms로 30배 격차. 어디서 그 시간이 사라진 걸까요?
대부분 자재 운송에 들어간 시간이에요. 5절에서 본 메모리 도로 폭 한계. 670MB × 32회 = 21.4GB를 80GB/s 도로로 나르려면 0.27초. 거기에 캐시 미스, 분기 예측 실패, 스레드 동기화 같은 자잘한 비용이 더해져서 약 700ms가 나오는 거예요.
이게 LLM 추론 컴파일러의 핵심 도전이에요. 이론치는 16ms인데 실측은 700ms. 그 사이 44배 격차를 어떻게든 줄여 가는 작업이 시리즈 후반부의 주제예요. Lumen은 v0.1.0에서 v0.5.0까지 6주 동안 이 격차를 10배 정도 좁혔어요. ggml(llama.cpp)은 같은 격차를 5년 정도 일해서 5배 정도 좁혔고요.
prefill과 decode가 다른 이유
여기까지 읽으셨다면 한 가지 의문이 들 수 있어요. “단어 하나 만들 때마다 같은 청사진을 다시 읽는다고 했는데, 입력이 점점 길어지면 어떻게 처리하나요?”
좋은 의문이에요. LLM은 두 가지 모드로 도시를 통과합니다.
모드 A — prefill (프롬프트 통과): 사용자가 입력한 문장을 처음 한 번 통과시키는 단계. “오늘 날씨 알려줘”라고 입력했으면 이 6단어를 한 번에 처리해요. 도시 비유로 풀면 6대의 자재 트럭이 줄지어 도시 입구로 들어가는 모습. 시설은 한 번 열어 두고 6대를 차례로 처리. 시설을 6번 새로 여는 게 아니라 한 번만 엽니다.
모드 B — decode (답 생성): prefill이 끝난 뒤 새 단어를 한 번에 1개씩 만드는 단계. 도시 비유로 풀면 자재 트럭이 한 대씩 들어와서 매번 시설을 새로 엽니다. 한 대 처리하고 시설 닫고, 다음 대 들어오면 또 시설 엽니다.
큰 차이가 있어요. prefill에서는 청사진을 한 번만 읽으면 됩니다. 자재 트럭 6대가 한 번에 들어왔으니 시설을 한 번 열고 6대를 차례로 처리. decode는 단어 32개를 만들려면 시설을 32번 열고 닫아야 해요.
그래서 prefill은 decode보다 단어당 시간이 훨씬 짧습니다. 실제 측정 비교:
llama.cpp (전 세계 표준 LLM 추론 도구), 8 thread 기준:
decode 128단어: 87 단어/초 (단어당 약 11.5ms)
prefill 128단어: 739 단어/초 (단어당 약 1.4ms)
─────────────────────────────
비율: prefill이 decode보다 약 8.4배 빠름이게 ChatGPT 같은 LLM이 답할 때 “처음 1초는 좀 걸리고 그 다음부터 단어가 흐르듯 나오는” 느낌의 정체예요. 사실은 prefill이 빠르고 decode가 느린 거예요. 사용자가 긴 질문을 입력하면 prefill이 길어지지만, 그게 워낙 빨라서 체감이 안 돼요. 답을 토해내기 시작하는 순간부터는 decode 모드인데, 그게 느리니까 “글자가 한 자씩 흐르듯” 보이는 거예요.
그 둘 사이의 8배 차이가 청사진을 몇 번 다시 읽느냐의 차이입니다. prefill은 청사진을 한 번 읽어서 단어 N개를 만들고, decode는 단어 N개 만들려면 청사진을 N번 읽어요. 메모리 운송 비용이 그만큼 차이 나는 거예요.
제가 짠 Lumen은 이 두 모드 중 처음엔 decode만 지원했고, v0.5에서 prefill 인프라까지 갖췄어요. 그 과정에서 “prefill batching이 5~10배 가속을 줄 거다”라는 가설을 세웠다가 측정으로 부정당하는 흥미로운 경험이 있는데, 그건 9편 측정-주도 의사결정에서 자세히 풀게요. 솔직히 일주일 작업이 회귀로 끝나서 좀 충격이었거든요. “당연히 빠를 거다”라고 생각했던 게 측정해 보니 오히려 2.9배 느렸어요.
GPU 없이 CPU로 짠 이유 – 메모리 대역폭 얘기
이건 좀 다른 얘기인데, 사이드 시작할 때 한 가지 결정해야 했어요. “GPU로 짤까 CPU로 짤까?” 보통 LLM이라면 GPU지만, Lumen은 CPU만 지원해요. 그 이유를 짧게 풀어볼게요.
5절에서 본 메모리 도로 폭이 결정적 요인이에요. 정리해 보면:
메모리 대역폭 비교:
일반 PC CPU (DDR5 6000): 약 80GB/s
최신 노트북 CPU (DDR5 5200): 약 50GB/s
엔비디아 RTX 4090 GPU (GDDR6X): 약 1000GB/s (12배 빠름)
데이터센터 H100 (HBM3): 약 3000GB/s (37배 빠름)GPU의 메모리 대역폭이 CPU보다 12~37배 빨라요. 같은 670MB 청사진을 통과시키는 데 GPU가 압도적으로 유리해요. ChatGPT 같은 큰 모델이 GPU에서만 실용적인 이유가 그거예요.
그런데 작은 모델(Qwen2.5-0.5B 같은 670MB)은 CPU에서도 충분히 빠르게 돌아요. ggml 기준 8 thread에서 단어 90개/초. 그 정도면 채팅용으로 답답하지 않아요. 그리고 사이드 프로젝트 학습 목적으로는 CPU 쪽 코드가 훨씬 투명해요. SIMD 명령어를 직접 짜볼 수 있고, JIT 코드 생성도 손쉽게 디버깅 가능. GPU 커널은 디버깅이 까다로워서 사이드로는 좀 무거워요.
그래서 결정은 “CPU만 지원, 메모리 도로 폭 한계를 정면으로 다루기”였어요. 이게 6주 사이드에 잘 맞는 선택이었던 것 같아요. 마이크로아키텍처 디테일(캐시·prefetch·branch predictor)까지 측정하고 만질 수 있었거든요.
1편 정리
이번 편에서 배운 거 한 번에 정리할게요. 이 5가지가 다음 편부터 나오는 모든 주제의 토대예요.
- AI 모델 = 거대한 도시 청사진. Qwen2.5-0.5B-Q8 기준 670MB, 약 6억 3천만 개의 8비트 정수. 24개 동(layer)이 순서대로 연결된 직선 도시. 한 동당 약 16MB 시설.
- 답 만들기 = 도시 한 번 통과해서 다음 단어 1개 결정 반복. 단어 32개 답이면 도시 32번 통과 = 매트멀 3840번.
- 시간의 95%가 매트멀에서 소비된다. RMSNorm·RoPE·attention 같은 작은 처리는 합쳐서 5%. 빠르게 만든다는 건 매트멀을 빠르게 만든다는 뜻.
- 진짜 병목은 계산이 아니라 자재 운송. 670MB 청사진을 단어 만들 때마다 다시 읽어야 함. 운송 시간이 계산 시간의 10~40배.
- prefill과 decode는 다른 모드. prefill은 자재 트럭 여러 대 한 번에 처리(빠름), decode는 한 대씩 처리(느림). 사용자 체감으로 “처음 1초 + 그 다음 흐르듯”이 그 둘의 차이.
참고로 이 글에서 다룬 Qwen2.5-0.5B는 작은 도시예요. 같은 회사의 1.5B(약 1.5GB), 7B(약 7GB), 72B(약 72GB) 같은 더 큰 모델들도 있는데 원리는 똑같습니다. 도시가 커지면 동(layer) 수가 30개, 40개로 늘고 각 시설의 결합표 크기도 커져요. 매트멀 한 번의 곱셈 횟수도 늘고 자재 운송 양도 비례해서 늘죠. 다만 동작 방식은 같아요. 큰 도시는 작은 도시의 확장판이지 완전히 다른 동작을 하는 게 아니에요. 이 글에서 0.5B로 익힌 개념은 7B에도 그대로 적용됩니다.
ChatGPT 같은 거대 모델이 어떻게 “이해”하는 것처럼 답하는지에 대한 답도 결국 이 구조예요. 청사진(시설들의 숫자 값)이 학습 과정에서 인간 언어의 통계 패턴을 압축해서 저장하고 있을 뿐, 추론 시점에는 단순한 청사진 통과예요. 우리가 컴파일러를 짜는 입장에서는 다행이에요. 이해가 뭔지 풀어야 한다면 막막하지만, “청사진을 빠르게 통과시키기”는 명확한 공학 문제거든요.
그리고 한 가지 더. 이 글에서 청사진의 weight 값이 어디서 왔는지는 다루지 않았어요. 그게 학습 단계의 일이고, 학습은 인터넷에 있는 수십 TB 데이터로 GPU 수천 장 돌려서 몇 달간 누적해서 만든 결과예요. Qwen 모델은 알리바바 클라우드가 학습시킨 거고, 우리는 그 결과만 받아서 추론하는 거예요. 추론 입장에서는 weight가 어디서 왔는지는 신경 쓸 필요 없고, 그냥 “이 670MB짜리 곱셈표 빠르게 통과시키기”가 목표.
다음 편 미리보기
2편에서는 “이걸 왜 직접 짜려고 했나”를 풀게요. llama.cpp(ggml)이라는 5년+ 누적된 표준 도구가 이미 있는데 그걸 안 쓰고 Rust로 처음부터 짠 이유. 책 100번 읽는 것과 한 번 직접 짜보는 것이 어떻게 다른지. 사이드 프로젝트를 9개월 흐지부지 못 끝낸 작년 경험까지 곁들여서 풀어볼게요.
3편부터는 Lumen이라는 컴파일러의 내부 구조로 들어가요. DSL·AST·IR이 뭐고 왜 필요한지부터 차근차근. 도시 청사진을 만드는 4단계 작업이 어떻게 나뉘는지의 연결입니다.
이 시리즈 본문 전체는 GitHub에도 정리해 뒀어요. github.com/redchupa/lumen/docs/tutorial에서 영어 원문 격으로 보실 수 있고, 블로그는 한국어 페르소나 톤으로 풀어 가는 버전이에요.
그럼 다음 편에서 뵐게요. 혹시 LLM 추론이나 컴파일러 사이드 시작하려는 분 계시면 댓글 남겨주세요. 같이 정리해 가면 더 재밌어질 거 같아요.