Karpathy의 GPT-2 재현 영상 4시간을 한 편으로: 네 단계와 그 숫자, 그리고 그 뒤에 바뀐 것
4시간짜리 영상의 뼈대를 정리했습니다. GPT-2(124M)를 만들고, A100 한 장에서 한 스텝을 1,000ms에서 90ms로 줄이고, GPT-3 논문 설정으로 10B 토큰을 학습해 HellaSwag에서 OpenAI 체크포인트를 넘기까지입니다. nanoGPT의 GELU가 GPT-2와 달라 logit이 최대 5까지 어긋난다는 점도 직접 확인했습니다.

Karpathy의 GPT-2 재현 영상 4시간을 한 편으로: 네 단계와 그 숫자, 그리고 그 뒤에 바뀐 것
Andrej Karpathy의 Let's reproduce GPT-2 (124M)는 2024년 6월 9일에 올라온 4시간 1분짜리 영상입니다. 2026년 10월 6일에 확인했을 때 조회수는 약 120만 회였습니다. 빈 파이썬 파일에서 시작해, 영상에서 쓰는 벤치마크 기준으로 가장 작은 GPT-2를 넘는 모델을 만드는 데서 끝납니다. 코드는 karpathy/build-nanogpt에 영상의 단계마다 커밋 하나씩 남아 있습니다(나중에 고친 것까지 합쳐 44개).
이 글은 영상을 따라 만들 시간은 없지만 무엇을 어떤 순서로 하는지 알고 싶은 분을 위한 요약입니다. 영상의 네 구간을 차례로 따라가고, 3구간 끝의 데이터·평가·실행 준비만 결과와 함께 4구간 절에서 다룹니다. 화면에 나온 숫자는 남겨 두고, 숫자마다 출처를 붙였습니다. 영상(타임스탬프), 저장소, 제가 직접 돌린 결과 중 하나입니다.
요약
- 1구간: GPT-2를 직접 짜고, 학습 전에 OpenAI 가중치를 넣어 맞는지부터 확인합니다. 공개된 체크포인트로 문장을 생성해 본 뒤에야 무작위 가중치로 바꿉니다.
- 2구간: A100 한 장에서 학습 한 스텝을 약 11배 빠르게 만듭니다. 16 × 1,024토큰 배치 기준 1,000ms에서 90ms까지, TF32, bfloat16,
torch.compile, Flash Attention, 어휘 크기 패딩 순서입니다. - 3구간: 학습 설정은 GPT-3 논문에서 가져옵니다. AdamW, 기울기 클리핑, 워밍업 후 코사인 감소, 행렬에만 weight decay, 약 50만 토큰 배치, 그리고 기울기 누적과 GPU 8장입니다.
- 4구간: FineWeb-Edu 10B 토큰을 A100 8장에서 약 2시간 학습합니다. HellaSwag에서 OpenAI GPT-2(124M)를 넘었고, 40B 토큰까지 늘린 실행은 33.24%로 GPT-3(124M)에 조금 못 미쳤습니다.
1구간: GPT-2를 짜고, 진짜 GPT-2인지부터 확인합니다 (00:13:47)
Karpathy는 모델을 평범한 PyTorch 모듈로 쓰는데, 파라미터 이름을 Hugging Face의 GPT-2와 똑같이 맞춥니다. 그래야 OpenAI가 공개한 가중치를 그대로 불러올 수 있기 때문입니다(28:08). 이어서 forward와 샘플링 루프를 쓰고 실제 체크포인트로 문장을 생성합니다(37:02). 이게 되고 나서야 무작위 가중치로 바꿔 학습을 시작합니다. 처음에는 Tiny Shakespeare 배치 하나를 외우게 해 보고(56:42), 그다음 작은 데이터 로더를 붙입니다.
이 구간에서 눈여겨볼 두 가지가 있습니다. 둘 다 이 글 끝에서 소개할 스피드런에서 다른 방식으로 바뀌었습니다(출력층을 따로 두고, 출력 투영을 0으로 초기화).
- 토큰 임베딩과 출력층이 행렬 하나를 같이 씁니다(1:06:14). GPT-2 small에서 이 행렬은 50,257 × 768, 약 3,860만 개 파라미터로 전체 124M의 31% 정도입니다.
- 초기화는 GPT-2 코드를 따릅니다. 표준편차 0.02의 정규분포를 쓰고, residual stream에 더해지는 출력 투영만 1/√(2 × 층 수)만큼 줄입니다. 층이 깊어져도 stream 값이 커지지 않게 하려는 것입니다(1:13:47).
직접 확인한 것: nanoGPT의 GELU는 GPT-2와 다릅니다
이 연재의 출발 코드는 karpathy/nanoGPT(커밋 3adf61e, MIT 라이선스)입니다. 영상 설명에 따르면 영상에서 만드는 코드는 결국 이 저장소와 "약 90% 비슷"해집니다. 학습을 돌리기 전에 1구간과 같은 확인을 했습니다. OpenAI의 124M 가중치를 넣고, 같은 8 × 1,024토큰에서 Hugging Face transformers 4.56.2와 logit을 비교했습니다.
맞지 않았습니다. CPU에서 float32로 비교했을 때 logit 차이가 최대 5.08이었습니다. 원인은 한 줄이었습니다. nanoGPT는 정확한 GELU를 쓰는데, OpenAI의 GPT-2와 Hugging Face의 gelu_new, 그리고 build-nanogpt의 nn.GELU(approximate='tanh')는 모두 tanh 근사를 씁니다. tanh 근사로 바꾸니 최대 차이가 0.0013으로 줄었고, 그 토큰들에서 loss는 둘 다 3.160816으로 같았습니다.
차이의 크기는 작습니다. 같은 GPT-2 가중치에서 GELU만 바꿨을 때 그 토큰들의 loss 차이는 0.00007 nats(3.160886과 3.160816)였습니다. 제가 잰 것은 여기까지입니다. 정확한 GELU로 모델을 처음부터 학습해 보지는 않았으니, 그렇게 학습하면 결과가 달라지는지는 이 글에서 확인하지 않았습니다. 다만 "이 모델이 GPT-2가 맞다"를 확인하려는 검사는 logit을 하나하나 비교하는 순간 분명하게 실패합니다. nanoGPT에 GPT-2 가중치를 불러올 생각이라면 GELU부터 바꾸시기 바랍니다.
가중치가 맞은 상태에서, OpenAI GPT-2(124M)는 이 연재에서 계속 쓸 FineWeb-Edu 검증 토큰(2,090만 개)에서 loss 3.297을 냅니다. 목표선이 아니라 참고선입니다. OpenAI는 다른 데이터(WebText)로 학습했기 때문입니다.
2구간: 한 스텝 1,000ms에서 90ms까지 (01:22:18)
이 구간의 시간은 모두 A100 SXM 80GB 한 장, 1,024토큰짜리 시퀀스 16개 배치에서 잰 값입니다. 단계별 숫자는 영상 챕터 제목에 있습니다.
| 바꾼 것 | 스텝 시간 | 하는 일 |
|---|---|---|
| float32 그대로 | 약 1,000ms | 모든 숫자를 32비트로 |
| TF32 행렬곱 | 333ms | 텐서 코어가 가수부를 줄인 채 fp32 행렬곱을 계산. set_float32_matmul_precision('high') 한 줄 |
| bfloat16 autocast | 300ms | 활성값을 16비트로. 지수 범위가 fp32와 같아 gradient scaler가 필요 없음 |
torch.compile | 130ms | 파이썬 오버헤드를 없애고 원소별 연산을 묶어 메모리 왕복을 줄임 |
| Flash Attention | 96ms | T × T 어텐션 행렬을 메모리에 쓰지 않음. 연산은 늘고 메모리 이동은 크게 줆 |
| 어휘 50,257 → 50,304 | 93ms | 50,304 = 128 × 393. 커널이 딱 떨어지는 크기를 타일 단위로 처리 |
| fused AdamW와 3구간 설정 | 90ms | 옵티마이저 갱신을 커널 하나로 |
표의 모든 줄이 가리키는 것은 하나입니다. GPU는 대부분의 시간을 계산이 아니라 메모리를 기다리며 보냅니다. 사양표에서 TF32는 fp32의 8배 연산량을 약속하지만 실제로는 약 3배 빨라지는 데 그쳤습니다(1:38:24). 이 스텝의 병목이 연산량이 아니었기 때문입니다. 그 아래 줄들은 모두 메모리에서 옮기는 바이트를 줄이는 방법입니다.
16,384토큰에 90ms면 GPU 한 장에서 초당 약 18만 2천 토큰입니다. 참고로 이 연재용으로 만든 제 학습 코드는 같은 모델을 GPU당 마이크로배치 64로 A100 PCIe 두 장에서 돌렸을 때 첫 학습 실행에서 초당 약 36만 1천 토큰(10스텝부터 1,580스텝까지 기록한 스텝 시간의 평균), 장당 약 18만 토큰이 나왔습니다. A100 종류, 배치, 코드가 모두 다르니 비슷한 수준이라는 정도로만 보시면 됩니다.
3구간: 학습 설정은 GPT-3 논문에서 (02:14:55)
GPT-2 논문에는 학습 방법이 거의 적혀 있지 않습니다. 그래서 Karpathy는 가장 작은 모델 크기가 같은 GPT-3 논문의 설정을 가져옵니다.
- AdamW, β = (0.9, 0.95), ε = 1e-8. 기울기는 노름 1.0으로 자릅니다
- 학습률은 715스텝 동안 선형으로 올리고(GPT-3 논문의 3억 7,500만 토큰과 같은 양), 그 뒤 최대값의 10%까지 코사인으로 내립니다. 최대 학습률은 6e-4입니다
- weight decay 0.1은 2차원 파라미터(행렬곱 가중치와 임베딩)에만 걸고, 편향과 LayerNorm 게인에는 걸지 않습니다
- 배치는 524,288토큰(2^19, 논문의 약 50만 토큰)입니다. GPU 한 장에 다 들어가지 않으니 기울기 누적으로 만듭니다(2:34:09). 작은 배치 여러 개의 기울기를 더한 뒤 한 번만 갱신하는 방식입니다. 이때 마이크로배치마다 loss를 마이크로배치 개수로 나눠야 합니다. 나누지 않으면 기울기가 그만큼 커진다는 것을 작은 예제로 보여 줍니다(2:42:43)
- DistributedDataParallel로 GPU 8장을 씁니다(2:46:52). GPU마다 다른 마이크로배치를 맡고, 갱신 전에 기울기를 평균합니다. GPU당 64 × 1,024토큰에 8장이면 누적 없이 한 번에 됩니다
학습률에 관해서는 이 연재와 바로 이어지는 이야기가 하나 더 있습니다. 4구간에 들어가서(3:51:35) Karpathy는 최대 학습률을 세 배쯤 올려도 된다는 사람들이 있었다며, GPT-3 설정이 "매우 보수적"일 수 있다고 말합니다.
4구간: 본 실행과 결과 (03:43:05)
본 실행 준비는 3구간 끝(3:10:21부터)에 나오지만 결과와 함께 여기서 정리합니다. 데이터는 Common Crawl에서 교육용 문서를 걸러 낸 FineWeb-Edu의 10B 토큰 샘플입니다. 1억 토큰짜리 샤드 100개로 나눠 씁니다. GPU 8장에서 초당 약 150만 토큰, 스텝당 약 330ms가 나와서, 10B 토큰을 한 번 도는 19,073스텝을 약 1.7시간으로 잡았습니다(3:21:39부터 3:22:13까지). 뒤에서는 이 실행을 "대략 두 시간"이라고 부릅니다(3:48:48). 화면의 실행은 torch.compile을 끈 상태입니다. 당시 compile이 평가와 샘플링 코드를 깨뜨렸기 때문입니다(3:38:55).
다음 날 아침의 결과는 이렇습니다.
- 검증 loss: FineWeb-Edu 검증 세트에서 같은 데이터로 평가한 OpenAI GPT-2(124M) 체크포인트보다 낮아졌습니다. 다만 GPT-2는 다른 분포로 학습했으니 "정확히 공정한 비교는 아니다"라고 Karpathy가 직접 말합니다.
- HellaSwag: 비교 기준으로 쓴 벤치마크입니다. 찍기 수준인 25%에서 출발해, 그의 평가 스크립트로 29.5%가 나오는 OpenAI GPT-2(124M)를 넘었습니다(3:37:16). GPT-2는 약 100B 토큰을 학습했는데 여기서는 10B였다는 점도 짚습니다.
- 40B 토큰 실행: 같은 데이터를 네 번 돈 실행(약 8시간)은 HellaSwag 33.24%였습니다. 300B 토큰으로 학습한 GPT-3(124M)에 조금 못 미칩니다(3:52:42).
한계도 본인이 먼저 짚습니다(3:45:30부터 3:47:10까지). FineWeb-Edu는 영어만 있고 코드와 수학이 적어서 모델 용량을 다른 데 쓰지 않습니다. HellaSwag는 오래된 벤치마크라 일부가 웹 데이터에 섞여 들어갔을 수 있습니다. 학습 loss가 주기적으로 출렁이는데, 샤드를 섞지 않고 정해진 순서로 읽은 탓으로 봅니다.
영상 뒤에 바뀐 것
nanoGPT는 더 이상 권장되지 않습니다. 2025년 11월부터 README 첫머리에 "now very old and deprecated"라는 문구와 함께 nanochat을 쓰라는 안내가 붙어 있습니다. nanochat은 토크나이저부터 채팅 모델까지 가는 Karpathy의 새 프로젝트입니다. 영상은 지금도 기초를 배우기에 좋지만, 새 작업을 시작한다면 출발점은 nanochat입니다. 그래도 이 연재는 일부러 nanoGPT에서 출발합니다. 나중 변경을 잴 기준은 GPT-2 그 자체여야 하는데, 위의 GELU 한 줄만 고치면 nanoGPT가 바로 그 GPT-2이기 때문입니다.
같은 목표가 스피드런이 됐습니다. modded-nanogpt는 Karpathy의 llm.c GPT-2 실행이 도달한 loss(FineWeb 검증 3.28, H100 8장에서 45분)를 목표로 정하고 기록표를 만들었습니다. README의 최신 기록은 2026년 8월 30일의 0.665분입니다. 같은 README는 지금은 학습 토큰 3억 3천만 개 미만으로 목표에 닿는다고 적고 있습니다. llm.c 실행은 10B 토큰이 필요했습니다.
기록표 앞쪽을 보면 GPT-2 이후 언어 모델에서 무엇이 바뀌었는지가 차례로 나옵니다. 학습률을 다시 맞춘 RoPE, Muon 옵티마이저, ReLU², 0으로 초기화한 투영과 QK norm, 묶지 않은 출력층, logit softcap 순입니다. 그런데 기록은 H100에서 잰 벽시계 시간이고, 한 기록에 여러 변경이 같이 들어간 경우가 많습니다. 그래서 이 표로는 무엇이 어떤 순서로 들어왔는지는 알 수 있어도, 변경 하나만으로 얼마나 좋아지는지는 알 수 없습니다.
이 연재는 그 질문을 다룹니다. 위에서 확인한 GPT-2를 출발점으로 두고, A100 두 장에서 한 번에 하나씩 바꿔 봅니다. 다만 무엇이든 비교하기 전에, 다음 글에서는 아무것도 바꾸지 않고 무작위 시드만 바꿨을 때 최종 loss가 얼마나 움직이는지부터 잽니다. 이후 글에서 어떤 차이를 진짜 차이로 볼지 그 숫자로 정합니다.
그 측정이 올라오는 날 알림을 받고 싶으시면 아래 뉴스레터 양식을 이용해 주세요.
이메일로 받아보기
관련 포스트

Karpathy의 autoresearch 해부 — 630줄로 AI 연구실 만들기
Andrej Karpathy의 autoresearch를 코드 레벨까지 분석합니다. 630줄 train.py, BPE 토크나이저, MuonAdamW 옵티마이저, 에이전트 프로토콜의 설계 원리를 파헤칩니다.

Karpathy의 microgpt.py 완전 해부: 150줄로 이해하는 GPT의 본질
PyTorch 없이 순수 Python 150줄로 GPT를 학습하고 추론하는 microgpt.py. 코드 한 줄 한 줄을 해부하며 GPT의 알고리즘과 효율화를 구분합니다.

나만의 LLM Knowledge Base 구축하기 — Karpathy 스타일 지식 시스템
Obsidian + Claude Code로 영구적인 개인 지식 체계를 만드는 완전 가이드. 위키 + 메모리 두 축의 지식 시스템.