Models & Algorithms••EN

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시간을 한 편으로: 네 단계와 그 숫자, 그리고 그 뒤에 바뀐 것

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 autocast300ms활성값을 16비트로. 지수 범위가 fp32와 같아 gradient scaler가 필요 없음
torch.compile130ms파이썬 오버헤드를 없애고 원소별 연산을 묶어 메모리 왕복을 줄임
Flash Attention96msT × T 어텐션 행렬을 메모리에 쓰지 않음. 연산은 늘고 메모리 이동은 크게 줆
어휘 50,257 → 50,30493ms50,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가 얼마나 움직이는지부터 잽니다. 이후 글에서 어떤 차이를 진짜 차이로 볼지 그 숫자로 정합니다.

그 측정이 올라오는 날 알림을 받고 싶으시면 아래 뉴스레터 양식을 이용해 주세요.

더 많은 콘텐츠를 받아보세요

SNS에서 새로운 글과 튜토리얼 소식을 가장 먼저 받아보세요

이메일로 받아보기

관련 포스트