GITHUB 레포 딥다이브 · 2026-08-04 · FAREEDKHAN-DEV/KIMI-K3-IN-C · 2.78조 파라미터 모델을 CPU 한 대, RAM 8GB에서 돌리는 순수 C99 추론 엔진

kimi-k3-in-c 딥다이브
"메모리를 하한선이 아니라 다이얼로 바꾼다" : 1.56TB 모델을 노트북에서 돌리는 176KB짜리 엔진

FareedKhan-dev/kimi-k3-in-c는 2.78조(2.78T) 파라미터짜리 초거대 MoE 모델 "Kimi K3"의 추론(inference)을 순수 C99로 다시 구현한 교육용 엔진이다. BLAS도, PyTorch 같은 프레임워크도, GPU도 쓰지 않는다. 놀라운 점은 디스크에 1.56TB나 되는 체크포인트를, 실제 사용 RAM 8.24GB에서 돌려 정답을 뽑아낸다는 것이다.

비결은 한 문장으로 요약된다 — "메모리를 반드시 채워야 하는 하한선(floor)이 아니라, 돌리고 싶은 만큼 돌리는 다이얼(dial)로 만든다." 무거운 부분(전문가 가중치 1.45TB)은 절대 RAM에 올리지 않고 디스크에서 흘려보내며(streaming), 압축된 4비트 형태 그대로 곱한다. 그 결과 똑같은 모델이 8GB에서도, 224GB에서도 돌아가고, 그 사이 어떤 메모리 예산에서도 바이트 단위로 똑같은 출력을 낸다. 차이는 오직 속도뿐이다.

카파시(Karpathy)의 llama2.c가 "작은 모델을 한 파일 C로"였다면, 이건 "프론티어급 초거대 모델을 노트북에서"를 증명한 살아있는 교재다. README 한 편이 143KB(교과서 한 권 분량)에 이르고, 모든 수치가 실제 측정값이며 측정 방법까지 공개돼 있다.

(저장소 FareedKhan-dev/kimi-k3-in-c · 라이선스 Apache-2.0 · 언어 순수 C99(엔진 코어 약 5,600줄·176KB) + Python 도구 · 버전 v0.1.0(첫 공개 2026-07-31) · 최신 커밋 2026-08-01 · 제작 Fareed Khan(@FareedKhan-dev) · 플랫폼 Linux x86-64 · TrendShift Daily #9)
목차
  1. 한 줄 정체
  2. 왜 주목받는가 — "불가능해 보이는 숫자"를 정직하게 증명
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 — 네 번의 감축 + 다섯 개의 불변식
  5. 디렉토리 구조 해부
  6. 학습 포인트 — 기술별 배울 것
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 로드맵 (주차별)
  10. 핵심 키워드 사전
  11. 참고 링크

1한 줄 정체

이 프로젝트가 정확히 무엇인지 한 문장으로

kimi-k3-in-c는 "디스크에 1.56TB로 저장된 2.78조 파라미터 모델 Kimi K3를, GPU 없이 CPU 한 대와 RAM 8GB만으로 한 토큰씩 추론해 내는, 프레임워크 의존이 전혀 없는 순수 C99 추론 엔진"이다. 한마디로 "초거대 모델을 노트북에서 돌리는 법"을 코드로 증명한 교과서다. 새 모델을 학습시키는 것도, 서비스로 배포하는 것도 아니다 — 이미 공개된 프론티어급 모델을, 가진 메모리가 아무리 작아도 돌아가게 만드는 "실행(추론) 엔진" 그 자체가 목적이자 결과물이다.

한 장의 비유

"창고에 든 백과사전 전집을, 책상 한 켠 작은 독서대만으로 읽어내기"

도서관 지하 창고(디스크, 1.56TB)에 백과사전 전집이 통째로 꽂혀 있다고 하자. 이 전집을 다 읽으려고 전부 책상 위(RAM)에 올리려 하면 책상이 수백 개 필요하다(= 수백 GB RAM). 보통의 방식이다.

kimi-k3-in-c는 다르게 접근한다. 책상 위엔 작은 독서대 하나(8GB)만 둔다. 지금 이 순간 필요한 책 몇 권만 창고에서 꺼내 독서대에 올려 읽고, 다 읽으면 도로 넣는다(= 스트리밍). 게다가 책을 이미 압축된 소형판 그대로 읽지, 굳이 큰 판형으로 확대 복사하지 않는다(= MXFP4 직접 연산). 그래서 독서대가 작아도 전집 전체를 읽어낸다. 대신 창고를 계속 오가야 하니 느리다 — 그게 이 마법의 유일한 대가다. 독서대를 크게 주면(RAM을 늘리면) 오가는 횟수가 줄어 빨라질 뿐, 읽어내는 내용(출력)은 글자 하나까지 똑같다.

용어
추론(inference) 엔진
이미 학습이 끝난 모델에 입력(프롬프트)을 넣어 다음 토큰을 예측·생성하는 실행기. 모델을 "훈련(training)"시키는 것과는 정반대 방향의 작업이다. 이 레포는 훈련 코드를 전혀 담지 않는다 — 오직 "돌리기"만 한다. 그래서 GPU도 대개 필요 없다(연산량이 훈련보다 훨씬 작다). llama.cpp·vLLM 같은 것이 대표적 추론 엔진인데, 이 레포는 그중에서도 "극한으로 작은 메모리"에 특화된 교육용 버전이다.
용어
MoE (Mixture of Experts, 전문가 혼합) · "활성 파라미터"
모델을 수백 개의 작은 신경망 조각("전문가")으로 나눠 두고, 토큰마다 그중 소수만 골라 쓰는 구조. Kimi K3는 층마다 896명의 전문가 중 16명만 뽑아 쓴다. 그래서 전체 파라미터는 2.78조로 거대하지만, 한 토큰이 실제로 건드리는 파라미터(활성 파라미터)는 약 1,040억(104B)뿐이다. 이 "전체는 크지만 매번 쓰는 건 일부"라는 성질이, 안 쓰는 전문가를 RAM에 안 올려도 되는 스트리밍의 근거가 된다.

왜 이게 어려운 문제인지부터 감을 잡자. 숫자 세 개만 기억하면 된다.

┌─────────────────────────────────────────────────────────────────┐ │ Kimi K3 를 그냥 통째로 RAM 에 올리려면? │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 전체 파라미터 2.78 조 (2,780,000,000,000 개) │ │ 디스크 체크포인트 1.56 TB ← SSD 하나를 통째로 먹는다 │ │ naive 하게 RAM 필요 1,000 GB+ ← 서버 클러스터급 │ │ │ │ ──────────── 이 엔진이 실제로 쓴 RAM ──────────── │ │ │ │ 측정된 최대 사용량 8.24 GB ← 웬만한 노트북에도 있다 │ │ 엔진 코드 크기 176 KB ← 이메일 첨부도 되는 크기 │ │ 필요 GPU 0 개 │ │ │ └─────────────────────────────────────────────────────────────────┘

1,000GB가 필요할 일을 8GB로 줄인 것 — 그 124배의 격차를 어떤 정확도 손실도 없이(!) 메운 방법을 설명하는 것이 이 레포의 전부다. README의 표현을 빌리면 "클러스터에서 노트북까지 내려오는 네 번의 결정, 그리고 맨 아래의 답은 맨 위의 답과 똑같다."

2왜 주목받는가 — "불가능해 보이는 숫자"를 정직하게 증명

비슷한 "미니멀 추론" 레포가 쏟아지는 판에서 이게 상위에 오른 이유

첫째, 헤드라인 숫자 자체가 강렬하다. "2.78조 파라미터. CPU 한 대. RAM 8GB." — 이 한 줄이 개발자 커뮤니티를 흔들었다. 카파시의 llama2.c가 "작은 모델을 순수 C 한 파일로"라는 낭만으로 별 수만 개를 모았다면, 이 레포는 그 낭만을 프론티어급(2.78조) 규모로 밀어붙였다. "큰 모델은 큰 하드웨어가 필요하다"는 상식에 정면으로 반례를 든 셈이다.

둘째, "메모리를 다이얼로"라는 발상이 깔끔하고 새롭다. 대부분의 경량화는 모델을 손상시켜(양자화·가지치기) 억지로 우겨넣는다. 이 엔진은 반대다 — 모델은 손대지 않고, "어디에 바이트를 둘 것인가(RAM vs 디스크)"만 조절한다. 그래서 8GB든 224GB든 출력이 바이트 단위로 동일하다. 정확도를 파는 게 아니라 시간을 파는 트레이드오프라는 점이 우아하다.

비유

사진을 줄이는 두 가지 방법. 4K 사진을 작은 폰에 담는 흔한 방법은 화질을 떨어뜨려(압축) 용량을 줄이는 것이다 — 화면엔 들어가지만 원본과 달라진다(=양자화 경량화). 다른 방법은, 원본 화질은 그대로 두고 지금 화면에 보이는 부분만 클라우드에서 조금씩 불러오는 것이다(=스트리밍). 화면이 작아도 원본을 손실 없이 본다. 느릴 뿐이다. kimi-k3-in-c는 두 번째 방식을 택했다. 모델을 흐리게 만들지 않고, 필요한 조각만 그때그때 디스크에서 불러온다.

셋째, 모든 수치가 "측정값"이고 측정 방법까지 공개된다. README에 등장하는 8.24GB·32.69초/토큰 같은 숫자는 어림값이 아니라 docs/data/ 폴더에 실측 로그(하드웨어 사양·저장장치 대역폭·메모리 사다리)까지 함께 박제돼 있다. 심지어 "이 수치는 재프리필(re-prefill) 때문에 부풀려졌으니 실제 디코드에선 더 낮을 것"이라고 자기 숫자의 한계까지 스스로 밝힌다. 과장 마케팅이 넘치는 AI 판에서 이 정직함이 신뢰를 만든다.

넷째, 가중치 없이도 정확성을 검증할 수 있다. 1.56TB를 내려받지 않고도 make test 한 줄이면 1분 만에 "이 엔진이 PyTorch 원본과 토큰 단위로 일치하는가"를 확인할 수 있다(작은 13층 오라클 모델로). 이건 교육 자료로서 결정적인 강점이다 — 배우는 사람이 거대 파일 없이도 코드의 정확성을 스스로 재현할 수 있으니까.

이 레포의 진짜 강점
"불가능해 보이는 위업" + "교과서급 문서" + "정직한 한계 고백"의 희귀한 조합

보통 화려한 벤치마크 숫자를 내세우는 레포는 문서가 얇거나 재현이 안 된다. 이 레포는 정반대다 — 143KB짜리 README가 한 컴포넌트씩 밑바닥부터 쌓아 올리는 강의이고, 모든 주장이 측정 로그로 뒷받침되며, "느리다·베이스 모델이라 대화가 안 된다·긴 프롬프트는 비현실적이다" 같은 약점을 먼저 큰 소리로 말한다. 화려한 카피 뒤에 견고한 엔지니어링과 정직함이 같이 있는, 흔치 않은 물건이다.

과장 주의 (정직한 경계)
"8GB에서 2.78조 모델"은 사실이지만, 실용 도구가 아니라 "증명·교보재"다

이 위업의 대가는 속도다. 8GB 예산에서 토큰 하나 뽑는 데 약 32초 걸린다(메모리를 224GB까지 늘려도 ~19초). 매 토큰마다 디스크에서 수십 GB를 읽어야 하기 때문이다. 게다가 베이스 모델이라 챗봇처럼 대답하지 않고 텍스트를 "이어 쓸" 뿐이고, 긴 프롬프트·청크 프리필·서빙 계층·비전은 아직 없다(그리디 디코딩만). 즉 "내 노트북에서 Kimi K3를 실무에 쓰자"가 아니라 "어떻게 이게 가능한지 배우자"가 이 레포의 올바른 용도다. Kimi K3 자체도 2026년 모델이라, 가중치는 Moonshot AI 배포처에서 직접 구해야 하고 이 레포엔 가중치가 전혀 없다.

3기술 스택 전체 지도

"의존성 0" — 컴파일러와 표준 라이브러리, 그리고 리눅스 시스템콜 몇 개가 전부

이 프로젝트의 스택에서 가장 놀라운 건 거의 아무것도 안 쓴다는 점이다. 딥러닝 추론이라면 으레 딸려 오는 CUDA·cuDNN·BLAS(수치연산 라이브러리)·PyTorch가 전부 없다. 곱셈 커널마저 손으로 짰다. 이 "의존성 없음"이 176KB라는 크기와 "어디서나 컴파일된다"는 이식성의 근원이다.

엔진 코어 (src/ · include/ · 순수 C99)

영역무엇을 쓰나역할
언어C99 (표준 C, make portable는 제네릭 AVX2 타깃)엔진 전체. 외부 라이브러리 링크 0개
병렬화OpenMP (#pragma omp)행렬 곱을 코어 수만큼 나눠 실행. 이게 유일한 "가속" 수단
SIMDAVX2 + FMA (인트린식)CPU 한 명령으로 여러 float를 동시 곱셈-누산. AVX-512는 불필요
디스크 I/OO_DIRECT · pread · posix_memalignOS 캐시를 건너뛴 직접 읽기. 스트리밍의 핵심 (리눅스 전용 이유)
계측getrusage · clock_gettime피크 RSS(실사용 메모리)·구간별 벽시계 시간 측정
vendored 3종json.h · tok.h · tok_unicode*.h미니 JSON 파서 + BPE 토크나이저(third_party/, Apache 라이선스)
용어
O_DIRECT · 왜 리눅스 전용인가
파일을 읽을 때 보통 OS는 방금 읽은 내용을 페이지 캐시(RAM)에 남겨 둔다("다음에 또 읽을지 모르니까"). 그런데 이 엔진은 매 토큰마다 트렁크 전체를 한 번씩 다시 읽기 때문에, 캐시에 남겨 봐야 다음 토큰 전에 밀려나 RAM만 잡아먹는 헛수고다. O_DIRECT는 "캐시 쓰지 말고 디스크→내 버퍼로 곧장 복사하라"는 지시다. 흥미롭게도 이 하드웨어에선 O_DIRECT(3.2GB/s)가 버퍼드 읽기(2.3GB/s)보다 빨랐다 — 보통의 기대와 반대라, 저자도 "다른 장비에선 다시 확인하라"고 적어 뒀다. 이 시스템콜이 리눅스 고유라서 엔진이 Linux x86-64 전용이다.

Python 도구 (tools/ · scripts/ · 추론엔 불필요)

엔진 자체는 C지만, 주변 도구는 Python이다. 이들은 추론 실행에는 관여하지 않고(그래서 make test엔 Python이 필요 없다), 준비·검증·측정을 맡는다.

빌드 · 검증 인프라

스택에서 배우는 것
"의존성을 안 쓰는 것"이 곧 이식성·검증가능성·교육성이 된다

BLAS나 PyTorch를 끌어오면 코드는 짧아지지만, 그 안에서 무슨 일이 일어나는지 배울 수 없고 빌드도 무거워진다. 이 레포는 곱셈 커널까지 직접 써서, "행렬 곱이 실제로 어떻게 도는가"를 눈으로 읽을 수 있게 만들었다. 의존성 0은 제약이 아니라 설계 목표였던 셈 — 덕분에 어디서나 1분 안에 빌드되고, 모든 줄이 학습 대상이 된다.

4아키텍처 심화 — 네 번의 감축 + 다섯 개의 불변식

이 레포의 진짜 알맹이. "1,000GB를 8GB로 줄인 네 개의 결정"과 "틀려도 티가 안 나는 다섯 개의 함정"

축 0 — 바이트는 세 군데에 산다 (설계의 전부)

모든 것의 출발점은 "2.78조 파라미터의 무게가 어디에 실리는가"를 세 덩어리로 가르는 것이다. 이 표가 곧 설계 전체다:

구성요소크기어디에 사나
라우팅 전문가1.45 TB절대 RAM에 안 올림 · 디스크에서 스트리밍 · MXFP4(4비트) 그대로
덴스 트렁크108.81 GB"다이얼" — 상주시키거나 스트리밍하거나 (매 토큰 전체 재독)
임베딩 + 최종 norm + lm_head4.70 GB"바닥(floor)" — 항상 상주
순환 상태(93층) + KV 캐시~5.3 GB + 위치당 2.37MB항상 상주 (KV 캐시는 --incremental일 때만)

핵심은 이렇다. 1.45TB는 절대 RAM에 안 내려앉고, 108.81GB는 조절 가능한 다이얼이며, 약 5.3GB만이 반드시 있어야 하는 바닥이다. "8GB에서 돈다"는 말은 곧 "5.3GB 바닥 + 아주 작은 트렁크 창(window) + 작은 전문가 캐시"로 굴린다는 뜻이다.

축 1 — 한 토큰이 생성되는 전체 흐름

프롬프트 "The capital of France is" │ ▼ ① 토크나이저 (src/tokenizer) released tiktoken.model 직접 읽어 텍스트→토큰ID │ ▼ ┌────────────────── 93개 디코더 층을 순서대로 통과 (매 토큰) ──────────────────┐ │ │ │ 층마다: 입력 → [KDA 또는 Gated MLA 어텐션] → [MoE 또는 덴스 FFN] │ │ 69개 KDA · 24개 MLA 92개 라우팅 · 1개 덴스 │ │ │ │ MoE 층에서: 896명 전문가 중 top-16 선택 → 그 16명 가중치를 디스크에서 가져와 │ │ (전문가 캐시 miss면 O_DIRECT pread) → MXFP4 그대로 곱셈 │ │ │ │ 트렁크(층별 고정 가중치)는: 상주분은 RAM, 나머지는 디스크에서 흘려 읽음 │ │ │ └─────────────────────────────────────────────────────────────────────────────────┘ │ ▼ ② lm_head → 다음 토큰 확률 → 그리디 선택(argmax) → " Paris" │ └──▶ (반복) 생성된 토큰을 다시 입력에 붙여 다음 토큰 예측

이제 "1,000GB → 8GB"를 만든 네 번의 감축(four reductions)을 하나씩 본다. 이게 이 레포의 심장이다.

감축 ① — 전문가는 이미 "반 바이트"로 온다 (MXFP4 직접 연산)

Kimi K3의 전문가 가중치는 애초에 MXFP4(4비트 부동소수) 형식으로 배포된다. 즉 파라미터 하나가 0.53125바이트밖에 안 된다. 보통의 추론 엔진은 이걸 계산 전에 fp32(4바이트)로 "펼쳐서(dequantise)" 쓴다. 이 엔진은 펼치지 않고, 압축된 4비트 니블(nibble) 그대로 곱한다(k3_matmul_mxfp4).

용어
MXFP4 · dequantise(역양자화)
MXFP4는 한 값을 4비트로 표현하는 저정밀 형식(OCP MX FP4 표준)이다. 4비트로는 16가지 값(0, 0.5, 1, 1.5, 2, 3, 4, 6과 음수들)만 표현하되, 32개 값마다 공유하는 8비트 지수(스케일)를 곱해 범위를 넓힌다. 값 = E2M1[니블] × 2^(스케일−127). dequantise는 이 압축된 4비트를 계산용 32비트 실수로 "펼치는" 것. 이 엔진의 통찰은 "펼치지 마라"이다 — 펼치는 순간 데이터가 8배로 부푼다.
왜 이게 결정적인가 (숫자로)
전문가 하나를 펼치면 17.5MB → 132MB, 한 토큰이면 194GB의 순수 "펼치기 낭비"

전문가 하나는 33,030,144개 파라미터를 17,547,264바이트(17.5MB)에 담는다. 이걸 fp32로 펼치면 132MB가 된다. 그런데 한 토큰은 92개 라우팅 층 × 16명 = 1,472명의 전문가를 건드린다. 만약 매번 펼친다면 토큰 하나당 194GB어치의 펼치기가 필요하다 — 스트리밍 설계가 통째로 무너진다. "압축된 채로 곱한다"는 이 한 가지 결정이 전체 구조를 떠받친다.

감축 ② — KDA : 메모리가 절대 안 커지는 어텐션

93개 층 중 69개는 KDA(Kimi Delta Attention)다. 일반적인 트랜스포머 어텐션은 문장이 길어질수록 KV 캐시가 선형으로 커진다(위치마다 키·값을 저장). KDA는 대신 고정 크기의 "순환 상태 행렬" 하나에 과거를 압축해 담는다 — RNN처럼. 그래서 문장이 아무리 길어도 메모리가 안 커진다. 이게 스트리밍 예산을 예측 가능하게 만드는 두 번째 기둥이다.

용어
선형 어텐션(linear attention) · 순환 상태(recurrent state)
보통의 어텐션은 새 토큰이 이전 모든 토큰을 일일이 돌아본다(그래서 길이의 제곱으로 비싸지고, KV 캐시가 계속 쌓인다). 선형 어텐션은 과거를 고정 크기 상태 행렬 S에 "요약"해 두고, 새 토큰은 그 요약만 본다. KDA는 여기에 채널별 "망각 게이트"(decay)를 더해 오래된 정보를 서서히 흐리게 한다. 덕분에 상태 크기가 일정해 무한히 긴 문장도 같은 메모리로 처리한다. 다만 이 엔진에선 순차 재계산(re-prefill) 방식이라 속도 이득은 제한적이고, 메모리 예측성이 주된 이득이다.

감축 ③ — MLA : 96개 헤드 대신 하나의 잠재 벡터

나머지 24개 층은 Gated MLA다. 96개 어텐션 헤드가 각자 키·값을 들고 있으면 KV 캐시가 폭발한다. MLA는 키·값을 하나의 저차원 "잠재(latent)" 벡터로 압축해 캐시한다 — 위치당 2.37MB로 억누른다. MLA 층은 1-기반 위치 4, 8, 12, …, 92, 그리고 93에 놓여, 마지막 두 층이 모두 MLA다(끝 층은 항상 전역 어텐션을 한다).

감축 ④ — 트렁크 스트리밍 : 하한선을 다이얼로 (진짜 마법)

여기가 제목의 그 문장이다. 층마다 반드시 쓰는 "덴스 트렁크"(108.81GB)를 전부 RAM에 올릴 필요가 없다. 고정된 층 순서대로 디스크에서 흘려 읽으면, "다음에 읽을 것"이 항상 미리 정해져 있어 디스크가 헛돌지 않는다(순차 읽기라 빠르다). 그래서 "트렁크 중 몇 층을 RAM에 고정(pin)할지"가 곧 메모리 다이얼이 된다 — 0층 고정(8GB)부터 90층 전부 고정(192GB+)까지.

용어
트렁크(trunk) · 링 버퍼 + 고정 접두부(pinned prefix)
트렁크는 전문가를 뺀 "층마다 항상 쓰는 가중치"(어텐션 투영·norm 등)를 말한다. 스트리밍 구현은 src/io/k3_trunk.c의 링 버퍼(ring buffer) + 고정 접두부다: 앞쪽 N개 층은 RAM에 눌러 두고(pin), 나머지는 작은 순환 버퍼로 "읽고 → 쓰고 → 덮어쓰기"를 돌린다. 메모리를 늘리면 pin하는 층 수만 늘 뿐, 코드도 출력도 그대로다. "메모리 = 하한선"이 "메모리 = 성능 다이얼"로 바뀌는 지점이다.

이 네 감축이 합쳐진 결과가 아래 "메모리 사다리"다. 실측값이며, 출력 토큰은 모든 줄에서 동일하다:

총 RAM 트렁크 고정 전문가 캐시 초/토큰 피크 RSS 출력 ────── ────────── ─────────── ─────── ──────── ────────────── 8 GB 0 층 28 슬롯 32.69 8.24 GB Paris (정답) 12 GB 0 층 159 슬롯 31.41 10.53 GB Paris (정답) 32 GB 11 층 615 슬롯 31.44 31.90 GB Paris (정답) 96 GB 43 층 2,073 슬롯 24.40 95.51 GB Paris (정답) 192 GB 90 층 4,386 슬롯 21.32 191.83 GB Paris (정답) 224 GB 90 층 6,208 슬롯 19.21 223.82 GB Paris (정답) ────── ────────── ─────────── ─────── ──────── ────────────── → 8GB 에서 224GB 까지, 메모리를 28배 늘려도 답은 그대로. 오직 속도만 1.7배.

축 2 — 전문가 캐시, 그리고 "참여하지 않던 캐시"의 발견

스트리밍하는 전문가에 LRU 캐시(src/cache/k3_cache.c, 배치 프리페치 포함)를 붙였다. 그런데 측정해 보니 흥미로운 사실이 나왔다 — 캐시가 8GB부터 64GB까지 히트율 36.24%로 완전히 평평했다. 메모리를 8배 늘려도 캐시 이득이 0이었던 것이다.

여기서 배우는 것 (진짜 값진 통찰)
캐시가 안 먹히는 게 "버그"가 아니라 "모델의 의도된 성질"이었다

왜 LRU가 평평했나? Kimi K3의 라우터가 Quantile Balancing 기법으로 전문가 사용을 일부러 고르게 편다(특정 전문가 쏠림 방지). 그런데 "고른 사용"이야말로 LRU가 가장 못 견디는 패턴이다(재사용 지역성이 없으니까). 즉 약한 캐시 성능은 엔진의 결함이 아니라 모델 설계의 결과다. 게다가 미래를 아는 이상적 정책(Belady)은 같은 64GB에서 61.74%까지 올라가 "메모리를 사는 것보다 정책을 바꾸는 게 더 남는 방향"임을 데이터로 짚는다. "성능이 안 나올 때, 원인이 내 코드인지 문제의 본질인지 측정으로 가른다"는 태도의 교과서다.

축 3 — 다섯 개의 불변식 : "돌아가는데 틀린" 함정들

k3.h 맨 위에 다섯 개의 불변식(invariants)이 못 박혀 있다. 이들의 공통점은 무섭다 — 어겨도 모델이 멈추지 않고, 유창한 텍스트를 뱉으며, 조용히 틀린다는 것이다. 초거대 모델을 밑바닥부터 재구현할 때 어디서 미끄러지는지를 압축한 목록이라, 그 자체로 배울 거리다:

#불변식어기면?
1A_log헤드별로 인덱싱(채널별 아님). 체크포인트는 head_dim개를 담지만 앞 num_heads개만 유효, 나머진 패딩디케이 게이트가 엉뚱한 값 → 조용히 다른 모델
2UT-transform 역행렬은 (I + Akk)^-1. 부호가 관례가 아니다부호 하나로 recurrence 붕괴
3Aqk는 대각선을 유지, Akk는 버린다둘을 같게 처리하면 미묘하게 오답
4MLA는 NoPE(위치인코딩 없음)지만, 64개 rope 차원은 여전히 존재하고 캐시된다. 회전만 없을 뿐rope 슬롯을 지우면 헤드 폭 192→128, 다른 모델
5MoE 라우팅 bias는 선택(selection)만 조종. 결합 가중치는 bias 없는 sigmoid 점수에서bias된 점수로 가중하면 같은 전문가 뽑아도 혼합비가 틀림
용어
"조용히 틀린다(silently wrong)"의 공포
프로그램이 크래시도 없고 에러 메시지도 없이, 겉보기엔 멀쩡한 결과를 내면서 실제로는 틀린 상태. 초거대 모델 재구현에서 가장 무서운 종류의 버그다 — LLM은 원래 "그럴듯한 텍스트"를 뱉으니, 아키텍처가 틀려도 출력이 여전히 유창해서 틀렸다는 걸 알아채기 어렵다. 그래서 이 엔진은 config 리더가 "모르는 필드가 있으면 기본값으로 때우지 말고 아예 로드를 거부"하고(잘못된 아키텍처로 도는 최악을 막으려고), 픽스처로 각 불변식을 못 박아 검증한다. "돌아가면 맞다"는 착각을 시스템적으로 부순 설계다.

5디렉토리 구조 해부

엔진 코어는 놀랄 만큼 작다 — 파일 이름만 훑어도 "한 토큰이 흐르는 길"이 보인다
kimi-k3-in-c/ (레포 루트, 275 파일 / 75MB — 대부분은 docs 이미지·PDF) ├─ include/k3/ │ ├─ k3.h ★ 공개 타입·config·"반드시 성립하는 5개 불변식" │ └─ k3_cfg.h config 리더 — 모르는 필드는 기본값 대신 "로드 거부" │ ├─ src/ ★ 엔진 코어 전체 (C/H 약 5,600줄, 컴파일 산출물 176KB) │ ├─ core/k3_ops.c (1,075줄) 모든 커널: RMSNorm·SiTU-GLU·ShortConv· │ │ KDA recurrence·Gated MLA·AttnRes·MoE·MXFP4/bf16 matmul │ ├─ cli/k3_run.c ( 898줄) k3 바이너리 (인자 파싱·생성 루프·리포트) │ ├─ io/k3_st.c ( 580줄) safetensors 리더 (직접 구현, 의존성 0) │ ├─ io/k3_trunk.c ( 428줄) ★ 트렁크 스트리밍: 링 버퍼 + 고정 접두부 │ ├─ io/k3_load.c ( 157줄) 전문가 하나당 pread 한 번으로 합쳐 읽기 │ ├─ cache/k3_cache.c ( 428줄) 라우팅 전문가 LRU + 배치 프리페치 │ ├─ model/k3_bind.c ( 448줄) 텐서를 이름으로 층 구조체에 바인딩 │ └─ tokenizer/k3_tok.h ( 227줄) released tiktoken.model 직접 로드 │ ├─ third_party/ json.h · tok.h · tok_unicode*.h (vendored, Apache) ├─ tools/ (Python) 오라클 생성·커널 검증·캐시 시뮬·예산 계산 ~20개 ├─ scripts/ download-model.sh · k3-doctor.sh · pack_trunk 래퍼 ├─ tests/ 가중치 없이 도는 픽스처 + 오라클 게이트(teacher/greedy/incremental) ├─ docs/ ★ ARCHITECTURE·PERFORMANCE·TUNING·ROADMAP + data/(실측 로그) + tech-report.pdf ├─ examples/ 01-hello.sh · 02-memory-budgets.sh · 03-long-prompt.sh ├─ benchmarks/ 벤치 스크립트 ├─ Makefile · CMakeLists.txt · README.md(143KB!) · CHANGELOG.md · NOTICE · LICENSE(Apache-2.0) └─ .github/ CI(GCC·Clang·ASan·UBSan·lint) · Dependabot · 이슈/PR 템플릿

구조가 알려주는 힌트 셋. 첫째, 엔진 전체가 C 파일 7개·약 5,600줄뿐이다 — 75MB짜리 레포의 대부분은 문서 이미지와 기술 리포트 PDF지, 코드가 아니다. "작은 코어 + 두꺼운 문서·검증"이 이 프로젝트의 무게중심이다. 둘째, 파일 이름이 곧 파이프라인 단계다: tokenizer → bind → (trunk/cache로 가중치 조달) → core/ops(계산) → cli(루프). 셋째, docs/data/실측 로그가 코드와 함께 커밋돼 있다 — 주장과 증거가 같은 저장소에 산다.

설계에서 배우는 것
"관심사 분리"가 파일 경계로 그대로 드러난다

계산(core/ops) · 가중치 조달(io·cache) · 모델 배선(model/bind) · 실행 루프(cli)가 깔끔히 갈려 있다. 특히 "가중치를 어디서 어떻게 가져오는가(트렁크 스트리밍·전문가 캐시)"가 계산 커널과 완전히 분리돼 있어서, 커널은 "가중치가 RAM에 있든 디스크에서 왔든" 신경 쓰지 않는다. 이 분리 덕에 "메모리 다이얼"이 커널을 건드리지 않고 io 층에서만 조절된다 — 큰 시스템을 층으로 자르는 법의 좋은 표본이다.

6학습 포인트 — 기술별 배울 것

"LLM 추론"만이 아니라 "메모리·I/O·검증"을 다루는 시스템 엔지니어링 교재로 읽으면 값지다

① "메모리를 다이얼로" — 계산과 스토리지를 분리하는 사고 (가장 값진 배움)

대부분은 "RAM에 안 들어가면 못 돌린다"고 생각한다. 이 레포는 "안 들어가면 흘려보내면 된다(stream)"는 발상의 전환을 실물로 보여 준다. 핵심은 계산 순서가 고정돼 있으면, 다음에 필요한 데이터를 미리 알 수 있고, 그러면 디스크에서 순차로 흘려 읽어도 느리지 않다는 것. 이 "예측 가능한 접근 순서 → 스트리밍" 패턴은 LLM뿐 아니라 대용량 데이터 처리·게임 에셋 로딩·DB 페이징 어디에나 이식된다.

② 양자화 없이 저정밀 형식을 "직접 연산"하기 (MXFP4 matmul)

압축된 4비트를 풀지 않고 그대로 곱하는 k3_matmul_mxfp4는, "데이터를 펼치는 순간 지는 게임"을 어떻게 피하는지 보여 준다. 저정밀 수 형식(fp4/int4)의 비트 레이아웃을 읽고 SIMD로 곱하는 커널을 뜯어 보면, 양자화된 모델을 효율적으로 돌리는 법의 밑바닥을 배운다. 니블 순서 같은 "관례일 뿐이지만 틀리면 조용히 망하는" 디테일도 함께.

③ 선형 어텐션 & 저차원 압축 어텐션 (KDA · MLA)

KV 캐시가 안 커지는 KDA(선형 어텐션 + 망각 게이트)와, 키·값을 잠재 벡터로 누르는 MLA(NoPE)를 순수 C 구현으로 볼 수 있다. 논문으로만 접하던 최신 어텐션 변형들이 실제 코드로 어떻게 도는지, recurrence 순서(디케이→읽기→쓰기→출력)가 왜 "load-bearing"인지 손으로 만질 수 있다.

④ MoE 라우팅의 밑바닥 (896 중 top-16 고르기)

sigmoid 점수 계산 → bias로 "선택만" 조종 → 뽑힌 전문가를 bias 없는 점수로 가중 결합. 이 미묘한 순서가 왜 중요한지(#5 불변식)를 코드로 배운다. 라우터가 어떻게 "전문가 쏠림"을 막는지(Quantile Balancing), 그리고 그게 캐시 성능에 어떤 역설적 대가를 치르는지까지.

⑤ "돌아가는데 틀린" 버그를 시스템으로 막기 (검증 문화)

이게 어쩌면 LLM 지식보다 더 값진 배움이다. config 리더가 모르는 필드에 기본값을 안 넣고 로드를 거부하는 설계, 가중치 없이 도는 오라클 게이트(PyTorch 참조와 토큰 단위 대조), 각 불변식을 픽스처로 못 박기 — "정확성을 어떻게 테스트 가능하게 만드는가"의 교과서다. 크래시 안 나는 버그를 잡는 법을 배운다.

⑥ 저수준 리눅스 I/O & 성능 계측 (O_DIRECT · getrusage)

O_DIRECT로 OS 캐시를 우회하는 이유, posix_memalign 정렬 요구, getrusage로 피크 RSS 재기, 구간별 벽시계로 "시간이 어디서 새는지" 나누기. 시스템 프로그래밍의 실전 기법이 응축돼 있어, C로 성능 민감한 코드를 짜려는 사람에게 그대로 참고서가 된다.

실습 아이디어(가볍게)
가중치 없이 "정확성 검증"부터 체험하기

1.56TB 없이도 이 레포의 정신을 맛볼 수 있다. git clonemake -j && make test만 돌려 보라 — 1분 안에 "엔진이 PyTorch 원본과 토큰 단위로 일치"하는 오라클 게이트가 통과하는 걸 볼 수 있다. 그다음 k3.h 맨 위 다섯 불변식을 읽고, src/core/k3_ops.ck3_matmul_mxfp4 하나만 정독해 보라. "4비트를 안 펼치고 곱한다"가 실제 코드로 어떻게 생겼는지 보이는 순간, 이 레포의 절반을 이해한 것이다.

7하드웨어 / 시스템 요구사항

"진짜 장벽은 RAM이 아니라 저장장치" — 1.56TB를 담을 빠른 디스크가 관건
항목필요한 것
OSLinux, x86-64O_DIRECT·posix_memalign·getrusage를 쓰므로 리눅스 전용. macOS·Windows 미지원
CPUAVX2 + FMA(2013년 이후 대부분의 x86 CPU). AVX-512는 불필요. make portable은 제네릭 AVX2 타깃
RAM8GB 이상 — 모든 프리셋이 동작. 메모리가 많을수록 빠를 뿐, 출력은 절대 안 변함
저장장치~1.7TB 여유 공간(1.56TB 체크포인트 + 109GB 패킹된 트렁크). 빠른 로컬 디스크(NVMe)일수록 결정적 — 매 토큰 수십 GB를 읽으므로
툴체인GCC ≥ 9 또는 Clang ≥ 10, GNU make 또는 CMake
Python3.9+ — 다운로드·패킹·분석 도구용. make test엔 불필요
모델 가중치레포에 없음. Kimi K3(2026, Moonshot AI)를 공식 배포처에서 직접 받아 해당 라이선스 준수 필요
현실 점검
"8GB 노트북"은 맞지만, "1.7TB 빠른 SSD"라는 숨은 요구가 있다

헤드라인은 "RAM 8GB"지만, 진짜 병목은 저장장치다. 1.56TB짜리 체크포인트를 담을 공간이 있어야 하고, 그게 느린 HDD면 토큰 하나에 몇 분씩 걸린다. 실측 환경도 3.2TB NVMe(측정 대역폭 5~6GB/s)였다. 즉 "메모리는 작아도 되지만, 스토리지는 크고 빨라야 한다" — 병목이 RAM에서 디스크로 옮겨 갔을 뿐이다. 또한 이건 Linux x86-64 전용이라, 애플 실리콘 맥이나 윈도우에선 그대로 돌지 않는다.

실행 경로 (준비 후 실제 명령)

목적명령
빌드·검증 (가중치 불필요)make -j && make test — 1분, 네트워크·모델 없이 오라클 게이트 통과 확인
내 머신 진단scripts/k3-doctor.sh — 어떤 프리셋이 맞는지, 저장장치가 얼마나 빠른지 보고
모델 받기·패킹scripts/download-model.sh(바이트 대조 검증) → tools/pack_trunk.py
추론 실행./bin/k3 ~/k3model --trunk ~/k3trunk --preset laptop --tok ~/k3model --prompt "..." --gen 8 --incremental
메모리 프리셋--preset laptop|desktop|workstation|server|max (측정된 메모리 사다리에서 도출)

8직접 해볼 수 있는 실습 과제

1.56TB 없이도 대부분 가능하다 — 난이도별 5개
난이도 ★ · 입문

가중치 없이 오라클 게이트 통과시키기 30분

git clonemake -j && make test. 1분 안에 "teacher forcing 32/32 · greedy 20/20 · incremental 20/20 · ENGINE MATCHES THE REFERENCE EXACTLY"가 뜨는 걸 확인하라. 그다음 일부러 커널 하나를 살짝 망가뜨려(예: k3_rmsnorm에서 eps를 빼 보기) 어떤 게이트가 어떻게 실패하는지 관찰하라. "정확성을 테스트로 못 박는다"는 게 무슨 뜻인지 몸으로 안다.

난이도 ★★ · 코드 읽기

MXFP4 matmul 커널 하나 정독 + 손으로 역양자화 2시간

src/core/k3_ops.ck3_matmul_mxfp4k3_mxfp4_dequant를 읽어라. 그리고 종이에 니블 하나(예: 0b0110)와 스케일 바이트를 주고 값 = E2M1[니블] × 2^(스케일−127)을 직접 계산해 보라. "16가지 값만으로 어떻게 넓은 범위를 표현하는가", "왜 니블 순서를 뒤집으면 조용히 틀리는가"를 이해하면 저정밀 추론의 핵심을 잡은 것이다.

난이도 ★★★ · 실험 재현

전문가 캐시 시뮬레이터로 "평평한 LRU" 재현하기 반나절

tools/sim_cache.pytests/fixtures/expert_trace.bin을 여러 용량에서 재생해, README가 말한 "8GB~64GB 히트율 36.24% 고정"을 직접 재현하라. 그다음 LRU 대신 다른 정책(예: 무작위 축출, 또는 Belady 근사)을 구현해 히트율이 어떻게 바뀌는지 실험하라. "메모리를 사느냐 정책을 바꾸느냐"를 데이터로 논증하는 경험이다.

난이도 ★★★★ · 이식·확장

다섯 불변식 중 하나를 "일부러" 어겨 보고 출력 관찰 1일

작은 오라클 모델에서 불변식 #4(rope 슬롯 유지)나 #5(bias는 선택만)를 코드로 위반해 보라. 크래시가 안 나고, 텍스트가 여전히 유창하지만, 참조와 토큰이 어긋나는 "조용히 틀림"을 직접 목격하라. 이 경험이 "돌아가면 맞다"는 착각을 영구히 깨 준다. (실제 K3 가중치가 없어도 오라클 픽스처로 가능.)

난이도 ★★★★★ · 도전

"메모리를 다이얼로" 패턴을 다른 문제에 이식하기 주말 프로젝트

src/io/k3_trunk.c의 링 버퍼 + 고정 접두부 아이디어를 완전히 다른 도메인에 적용해 보라 — 예컨대 "RAM에 다 안 들어가는 거대한 이미지/데이터셋을, 접근 순서가 고정된 파이프라인에서 스트리밍 처리"하는 미니 프로젝트. 핵심은 "예측 가능한 접근 순서 → 순차 스트리밍 → 메모리를 성능 다이얼로". 이 패턴을 손수 옮겨 보면 이 레포의 진짜 교훈이 LLM을 넘어선다는 걸 안다.

9관련 기술 심화 로드맵 (주차별)

"LLM 추론 밑바닥 + 시스템 프로그래밍"을 함께 쌓는 6주 코스
주차주제무엇을 · 어떻게
1주트랜스포머 추론의 기초카파시 llama2.c / llm.c를 먼저 읽어 "순수 C로 LLM 추론"의 골격(임베딩·어텐션·FFN·샘플링)을 잡는다. 그다음 이 레포의 cli/k3_run.c 생성 루프와 비교.
2주저정밀 수 형식 & 양자화fp16·bf16·int8·MXFP4/int4의 비트 레이아웃. OCP MX 표준 훑기 → k3_mxfp4_dequant / k3_matmul_mxfp4 정독. "펼치지 않고 곱하기"의 의미 체득.
3주최신 어텐션 변형선형 어텐션(RWKV·Mamba 계열)과 KDA, 저차원 압축 MLA(DeepSeek 계열). k3_ops.ck3_kda_layer·k3_mla_cached를 논문과 대조.
4주MoE와 라우팅top-k 게이팅·전문가 병렬·로드 밸런싱(Quantile Balancing). k3_router·k3_moe + docs의 캐시 분석으로 "고른 사용 vs 캐시 지역성"의 긴장 이해.
5주시스템 I/O & 스트리밍O_DIRECT·mmap·페이지 캐시·NVMe 대역폭·큐 깊이. k3_trunk.c(링 버퍼)·k3_cache.c(LRU+프리페치)·k3_st.c(safetensors) 정독.
6주정확성 검증 & 재현성오라클 게이트·teacher forcing·픽스처 회귀·ASan/UBSan·"기본값 거부" config. 이 레포의 tests/·tools/verify_*.py를 자기 프로젝트에 이식하는 연습.

10핵심 키워드 사전

이 레포를 읽다 마주치는 용어를 한자리에
용어
MoEMixture of Experts. 층마다 수백 전문가 중 소수(K3: 896중 16)만 골라 쓰는 구조. 전체는 크지만 토큰당 활성 파라미터는 작다(~104B)
MXFP44비트 부동소수 형식(OCP MX FP4). 32개마다 공유 지수(E8M0)를 곱함. 파라미터당 0.53125바이트. K3 전문가가 이 형식으로 배포됨
트렁크 스트리밍전문가를 뺀 층별 고정 가중치(108.81GB)를 RAM 상주 대신 고정 순서로 디스크에서 흘려 읽기. "메모리를 다이얼로" 만드는 핵심
KDAKimi Delta Attention. 선형 어텐션 + 채널별 망각 게이트. KV 캐시가 안 커짐(고정 크기 순환 상태). K3 93층 중 69층
MLA(Gated) Multi-head Latent Attention. 키·값을 저차원 잠재로 압축 캐시(위치당 2.37MB). NoPE 사용. K3 24층
NoPENo Positional Encoding. 위치 회전을 안 함. 단 rope 차원(64) 슬롯은 존재·캐시됨(지우면 헤드 폭이 바뀜) — 불변식 #4
SiTU-GLUK3의 활성화 함수. tanh·sigmoid 조합에 β₁=4·β₂=25로 게이트 상한을 100으로 묶음
safetensors텐서를 안전하게 저장하는 파일 포맷. 헤더(JSON) + 원시 바이트. k3_st.c가 의존성 없이 직접 파싱
O_DIRECTOS 페이지 캐시를 우회해 디스크→버퍼 직접 읽기. 매 토큰 재독하는 스트리밍에 최적(캐시가 헛수고라서)
피크 RSSResident Set Size의 최대치 = 프로세스가 실제로 점유한 물리 메모리 정점. getrusage로 측정. "8.24GB"가 이 값
오라클 게이트PyTorch 참조 출력("정답")과 C 엔진 출력을 토큰 단위로 대조하는 테스트. teacher forcing·greedy·incremental 3종
Quantile Balancing라우터가 전문가 사용을 고르게 펴는 학습 기법(tech report §2.3.3). 부작용으로 LRU 캐시 지역성을 떨어뜨림
조용히 틀림크래시·에러 없이 겉보기 멀쩡한 결과를 내며 실제론 틀린 상태. LLM 재구현 최대의 적(유창해서 안 들킴)
그리디 디코딩매 스텝 가장 확률 높은 토큰(argmax)만 뽑는 방식. 이 엔진은 샘플링 없이 그리디만 지원

11참고 링크

원본과 더 깊이 파고들 자료