같은 GPT-2, 같은 데이터, 시드만 셋: 최종 loss가 0.018 벌어졌습니다
GPT-2(124M)를 FineWeb-Edu 10억 토큰으로 시드만 바꿔 세 번 학습했더니 검증 loss가 3.668, 3.669, 3.686으로 끝났습니다(표준편차 0.010). 시드 0을 같은 명령으로 다시 돌리면 0.0035 차이가 났습니다. 조건마다 시드 3개를 쓰면 0.016 nats를 넘어야 차이로 셉니다.

같은 GPT-2, 같은 데이터, 시드만 셋: 최종 loss가 0.018 벌어졌습니다
GPT-2(124M)를 같은 10억 토큰으로, 같은 순서로, 같은 설정으로 세 번 학습했습니다. 세 실행 사이에 바뀐 것은 처음 가중치를 정하는 난수 시드 하나뿐입니다. 최종 검증 loss는 3.6681, 3.6690, 3.6858이었습니다.
이 0.018 nats의 폭이 이 연재의 잣대가 됩니다. 모델을 바꿨는데 loss가 이보다 적게 움직였다면, 아직 아무것도 보여 준 것이 없습니다.
이 글은 GPT-2(124M)에 이후 학습 레시피들이 바꾼 것을 하나씩 더해 보는 연재의 첫 측정입니다. RoPE, RMSNorm과 QK norm, ReLU², Muon 옵티마이저, 0으로 초기화한 투영, logit softcap을 차례로 잴 예정입니다. 출발 모델은 앞 글에서 정리한 Karpathy의 GPT-2 재현에서 왔습니다. 그런데 무엇을 바꾸기 전에 먼저 알아야 할 것이 있습니다. 같은 모델을 두 번 학습하면 결과가 얼마나 떨어져서 나오는가입니다. 이후 비교가 얼마나 믿을 만한지는 이 답이 정합니다.
요약
- 시드 세 개의 최종 loss 폭은 0.0177 nats였습니다(표준편차 0.0100). 다만 실행 세 번으로는 표준편차를 정확히 잡을 수 없습니다. 95% 구간이 0.005부터 0.063까지입니다.
- 이번 실행에서 시드의 영향은 오르내림보다 고정된 자리 차이에 가까웠습니다. 시드 하나가 5억 토큰 지점부터 끝까지 세 시드 평균보다 0.010에서 0.012 높은 자리에 머물렀고, 따라잡지 못했습니다. 처음 두 평가 지점에서는 다른 시드가 가장 나빴습니다.
- 같은 시드를 같은 명령으로 다시 돌리면 0.0035 차이로 끝났습니다. 시드 간 폭의 5분의 1쯤이고, 표준편차와 비교하면 3분의 1쯤입니다. 학습 초반에는 두 실행이 0.029까지 벌어져 그 시점의 시드 간 폭보다 컸는데, 중반에 다시 붙었습니다.
- 연재에 주는 의미: 조건마다 시드 3개를 쓰면 차이가 0.016 nats를 넘어야 차이로 셉니다. 실행 전에 정해 둔 규칙대로이고, 그래서 다음 글부터는 조건마다 시드를 2개가 아니라 3개씩 돌립니다.
어떻게 돌렸는지
모델은 GPT-2 small입니다. 12층, 폭 768, 헤드 12개, 문맥 1,024토큰이고 입력 임베딩과 출력층이 행렬을 같이 씁니다. 코드는 nanoGPT(커밋 3adf61e, MIT 라이선스)에서 GELU 하나만 GPT-2 원본과 같은 tanh 근사로 바꾼 것입니다. OpenAI 가중치를 넣으면 같은 토큰에서 Hugging Face의 GPT-2와 loss가 같게 나오니, 출발점은 GPT-2와 비슷한 무언가가 아니라 GPT-2 그 자체입니다.
| 설정 | |
|---|---|
| 데이터 | nanochat이 쓰는 FineWeb-Edu 샤드, GPT-2 토크나이저, 모든 실행에서 같은 순서로 읽음 |
| 예산 | 1,907스텝 × 524,288토큰 = 학습 토큰 10억 개 |
| 옵티마이저 | AdamW, β (0.9, 0.95), 행렬에만 weight decay 0.1, 기울기 클리핑 1.0 |
| 학습률 | 최대 6e-4, 워밍업 100스텝, 코사인으로 10%까지 감소(GPT-2·GPT-3 값이고 이 예산에 맞춰 고르지 않음) |
| 정밀도와 속도 | bfloat16 autocast, torch.compile, PyTorch SDPA 어텐션 |
| 하드웨어 | A100 80GB PCIe 두 장, 데이터 병렬. 한 번에 약 49분 |
| 검증 | 모든 실행에 같은 검증 토큰 2,090만 개. 250스텝마다, 그리고 마지막에 잼 |
이 설정 그대로 네 번 돌렸습니다.
- 시드 0, 1, 2. 시드는 처음 가중치를 정하는 난수입니다. 그 밖에는 아무것도 바꾸지 않았습니다. 데이터도, 순서도, 설정도 같습니다.
- 시드 0을 한 번 더. 세 번을 다 돌린 뒤 똑같은 명령으로 다시 시작했습니다. 시드가 같으니 처음 가중치도 같습니다. 학습 코드가 시작할 때 임베딩 행렬의 해시를 기록하고, 두 시드 0 실행의 해시가 다르면 분석 스크립트가 멈추게 해 두었습니다. 그런데도 결과가 달라진다면 그 차이는 시드가 아니라 GPU에서 온 것입니다.
두 번째 종류의 실행을 넣은 이유가 있습니다. GPU 학습은 기본 설정에서 비트 단위로 같은 결과를 보장하지 않습니다. 어텐션 역전파와 두 카드 사이의 기울기 평균 과정에서 실행마다 숫자를 더하는 순서가 달라질 수 있고, 부동소수점 덧셈은 순서가 바뀌면 결과가 미세하게 달라집니다. 이 차이가 학습이 끝났을 때 남는지는 짐작할 일이 아니라 재 볼 일입니다.
질문과 실행 목록, 무엇을 답으로 볼지는 첫 실행을 시작하기 전에 적어서 커밋했습니다. 분석 스크립트는 첫 실행이 끝나기 전에 커밋했습니다. 실행 하나는 다시 돌렸습니다. 시드 0의 첫 시도가 1,907스텝 중 1,580스텝에서 작업 세션이 다시 시작되면서 함께 종료됐기 때문입니다. 최종 loss가 없어서 결과 폴더에 따로 남겨 두고 쓰지 않았습니다. 아래 네 실행은 모두 처음부터 끝까지 돈 것입니다.
최종 loss
| 실행 | 최종 검증 loss | 세 시드 평균과의 차이 |
|---|---|---|
| 시드 0 | 3.6681 | −0.0062 |
| 시드 1 | 3.6858 | +0.0115 |
| 시드 2 | 3.6690 | −0.0053 |
| 시드 0 재실행 | 3.6646 | −0.0097 |
세 시드의 평균은 3.6743, 표준편차는 0.0100, 가장 좋은 것과 나쁜 것의 차이는 0.0177입니다. 네 번째 줄은 네 번째 시드가 아니어서 평균에 넣지 않았습니다. 이 줄은 다음 절에서 다룹니다.
크기를 가늠할 기준도 하나 있습니다. 세 시드 평균으로 보면 학습 마지막 157스텝(토큰 8,200만 개) 동안 검증 loss가 3.6918에서 3.6743으로 0.0175 내려갔습니다. 시드 간 최대 차이가 학습 마지막 8% 구간이 벌어 준 만큼과 비슷하다는 뜻입니다. 다만 끝 무렵에는 학습률이 거의 최저라서, 이 비교는 크기를 짐작하는 정도로만 보시면 됩니다.
참고로 OpenAI가 공개한 GPT-2(124M)는 같은 검증 토큰에서 3.297이 나옵니다. 다른 데이터로 훨씬 많은 토큰을 학습한 모델이라, 이 차이는 10억 토큰이 짧은 예산이라는 것 말고는 이번 실행에 대해 알려 주는 것이 없습니다.
같은 시드를 두 번 돌렸을 때
시드 0의 두 실행은 같은 가중치에서 출발했고(기록한 임베딩 해시가 같습니다) 같은 데이터를 봤습니다. 평가 지점마다 검증 loss는 이렇습니다.
| 학습한 토큰 | 시드 0 | 시드 0 재실행 | 차이 |
|---|---|---|---|
| 0.13B | 5.5643 | 5.5824 | 0.0182 |
| 0.26B | 4.7051 | 4.7340 | 0.0288 |
| 0.39B | 4.1666 | 4.1887 | 0.0221 |
| 0.52B | 3.9411 | 3.9413 | 0.0002 |
| 0.66B | 3.8147 | 3.8126 | 0.0021 |
| 0.79B | 3.7329 | 3.7293 | 0.0036 |
| 0.92B | 3.6856 | 3.6822 | 0.0034 |
| 1.00B(최종) | 3.6681 | 3.6646 | 0.0035 |
GPU만으로도 결과가 움직였다는 뜻입니다. 0.26B와 0.39B 지점에서는 두 실행의 차이가 같은 시점의 서로 다른 세 시드 사이의 폭보다 컸습니다(0.0288 대 0.0163, 0.0221 대 0.0102). 0.13B 지점에서는 둘이 비슷했습니다(0.0182와 0.0187). 5억 토큰 무렵 다시 붙었고, 끝에는 0.0035 차이로 끝났습니다.
계획에 없던 시드 0 실행이 하나 더 있습니다. 1,580스텝에서 중단된 첫 시도입니다. 이 시도를 완주한 시드 0과 비교해도 같은 모양이 나옵니다. 0.26B 지점에서 0.0085 차이였다가 0.52B, 0.66B, 0.79B에서는 0.001 아래였습니다. 판정 규칙에 들어가는 비교는 아니지만 같은 방향을 가리킵니다.
차이가 어디서 오는지는 따로 가려내지 않았습니다. 후보는 어텐션 역전파 커널과 두 카드 사이의 기울기 합산 순서입니다. 둘을 나누려면 결정적 커널로 다시 돌려야 하는데, 그 설정은 느리고 이번 실행이나 대부분의 학습이 쓰는 설정도 아닙니다.
학습 내내 어떻게 움직였는지

| 학습한 토큰 | 시드 0·1·2 평균 | 표준편차 | 폭 |
|---|---|---|---|
| 0.13B | 5.5714 | 0.0101 | 0.0187 |
| 0.26B | 4.7146 | 0.0085 | 0.0163 |
| 0.39B | 4.1726 | 0.0054 | 0.0102 |
| 0.52B | 3.9468 | 0.0090 | 0.0160 |
| 0.66B | 3.8211 | 0.0095 | 0.0173 |
| 0.79B | 3.7389 | 0.0099 | 0.0174 |
| 0.92B | 3.6918 | 0.0100 | 0.0178 |
| 1.00B | 3.6743 | 0.0100 | 0.0177 |
두 가지가 눈에 띕니다. 첫째, 학습이 진행돼도 시드 간 표준편차가 줄지 않았습니다. 한 지점을 빼면 내내 0.01 근처였습니다. 둘째, 순위가 한 번 바뀐 뒤 그대로 굳었습니다. 처음 두 지점에서는 시드 2가 가장 나빴는데, 0.39B부터는 매번 시드 1이 가장 나빴습니다. 0.52B부터는 가장 좋은 시드와의 차이도 매번 비슷했습니다(0.016에서 0.018). 시드 1이 어떤 출발점을 받았든, 10억 토큰 안에서는 그 차이가 씻겨 나가지 않았습니다.
다음 글부터 이 숫자를 쓰는 방법
실행 전에 이후의 모든 비교에 쓸 규칙을 하나 정해 두었습니다. 설정마다 시드를 n개 돌리고, 두 설정의 평균 loss 차이가 2 × σ × √(2/n)보다 클 때만 다르다고 씁니다. σ는 시드 간 표준편차입니다. σ = 0.0100을 넣으면 이렇습니다.
| 설정당 시드 수 | 차이로 셀 수 있는 최소 크기 |
|---|---|
| 2 | 0.0199 |
| 3 | 0.0163 |
둘 중 무엇을 쓸지도 미리 정했습니다. 2σ가 0.01 nats 이하면 2개, 넘으면 3개입니다. 결과는 0.0199였으니 다음 글부터는 설정마다 시드를 3개씩 돌립니다. 한 번에 49분이니 설정 하나에 GPU 시간이 2시간 30분 안팎 듭니다.
σ가 0.0100에 머물지는 않습니다. 다음 글마다 설정당 실행이 3개씩 늘고, 규칙은 그 실행들의 시드 분산까지 합쳐서 σ를 다시 구합니다. 그러니 연재가 진행될수록 추정이 정확해집니다. 그렇게 다시 구한 값이 0.0100보다 커지면 기준선도 같이 올라가고, 오늘이라면 차이로 셌을 것 중 일부는 차이로 세지 않게 됩니다.
기준선보다 작은 차이는 "이 예산에서 구분되지 않는다"고 쓰고, "같다"고는 쓰지 않습니다. 시드 3개로는 0.01 nats짜리 실제 효과를 놓칠 수 있습니다. 놓쳤다고 해서 효과가 없다는 뜻은 아닙니다.
이 글로 알 수 없는 것
- 데이터 순서는 고정했습니다. 이 연재의 모든 비교가 그렇게 하기 때문에 모든 실행이 같은 토큰을 같은 순서로 읽었습니다. 대부분의 학습처럼 실행마다 데이터를 다르게 섞으면 흔들림은 적어도 이만큼, 아마 그보다 클 것입니다. 이 부분은 재지 않았습니다.
- 모델 크기 하나, 예산 하나입니다. GPT-2 small을 10억 토큰까지만 봤습니다. 더 길게 학습하면 시드 1의 차이가 씻겨 나갈 수도, 그렇지 않을 수도 있습니다.
- 시드 3개로는 표준편차를 느슨하게만 잡습니다. 95% 구간이 0.005부터 0.063까지이고, 규칙은 그 가운데 점 추정값을 씁니다.
- 규칙은 표준오차 두 배를 쓰는 거친 검정입니다. 실행이 이 정도로 적으면 t 검정은 더 높은 기준선을 요구합니다. 연재 전체를 같은 잣대로 비교하려고 미리 정한 단순한 규칙을 그대로 쓰지만, 이 규칙은 차이가 있다는 쪽으로 기울어 있습니다.
- GPU만으로 생긴 차이는 이 설정에서 잰 값입니다. PyTorch 2.8, SDPA 어텐션, A100 PCIe 두 장의 데이터 병렬입니다. 다른 커널, 다른 GPU 수, 결정적 모드에서는 다른 숫자가 나옵니다.
계획서, 스크립트, 네 실행(과 중단된 시도)의 원시 로그는 이 연재의 무료 코드 다운로드에 있습니다(로그인하고 받을 수 있습니다). 원시 로그로 이 글의 숫자와 그림을 그대로 다시 만드는 노트북이 들어 있고, 계획, 실행, 분석 순서를 보여 주는 커밋 시각도 함께 들어 있습니다.
재현 패키지
이 글의 하네스, 원 로그, 결과 CSV를 묶었습니다. 로그인 없이 받을 수 있고, GPU 없이 표의 숫자를 다시 계산하는 명령이 README에 있습니다.
gpt2-noise-floor-repro.zip · 132 KB
이메일로 받아보기
관련 포스트

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

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

VibeTensor: AI가 딥러닝 프레임워크를 직접 만들 수 있을까?
NVIDIA 연구진이 공개한 VibeTensor는 LLM 기반 AI 에이전트가 생성한 완전한 딥러닝 런타임입니다. 6만 줄 이상의 C++/CUDA 코드가 AI에 의해 작성되었으며, 이 프로젝트가 보여주는 가능성과 한계를 분석합니다.