Karpathy microgpt.py 해부: GPT 학습과 추론이 한 파일에서 돌아가는 방식
메뉴

AI Development

Karpathy microgpt.py 해부

순수 Python 한 파일에서 tokenizer, autograd, attention, Adam, sampling이 어떻게 이어지는지 따라갑니다.

Karpathy microgpt.py 해부 hero image
Markdown약 6789 tokens

GPT를 제대로 이해하려고 코드를 열면, 의외로 본질보다 주변 장치가 먼저 쏟아집니다.

PyTorch, CUDA, tokenizer, dataset loader, optimizer, scheduler, checkpoint, distributed training. 실무에서는 모두 필요한 장치입니다. 하지만 “GPT는 실제로 무엇을 계산하는가”를 처음 붙잡으려는 순간에는 이 장치들이 시야를 가립니다.

Andrej Karpathy의 microgpt.py가 좋은 이유는 작아서가 아닙니다. 숨겨진 레이어를 거의 모두 벗겨내고, GPT 학습 루프를 한 파일 안에서 끝까지 보이게 만들기 때문입니다. 문자 tokenizer에서 시작해 scalar autograd, Q/K/V attention, negative log likelihood, Adam update, autoregressive sampling까지 이어지는 경로가 그대로 드러납니다.

이 글의 목표는 “작은 GPT 구현을 소개하는 것”이 아닙니다. loss.backward()optimizer.step() 뒤에서 어떤 계산이 벌어지는지, attention이 왜 Q/K/V로 나뉘는지, 다음 token 학습이 실제 코드에서는 어떤 루프로 바뀌는지 확인하는 것입니다.

microgpt.py는 200라인 안팎의 짧은 파일입니다. 그래서 이 글은 원문을 피해서 설명하기보다, 원문 흐름을 따라 핵심 코드 블록을 충분히 싣고 바로 아래에서 해체하는 방식으로 읽습니다. 발행 시점의 최신 원본은 Karpathy의 microgpt.py gist에서 다시 확인하는 편이 안전합니다.

분석 기준일: 2026-05-05
분석 대상: Andrej Karpathy microgpt.py gist
분석 방식: 원문 흐름을 따른 핵심 코드 블록 발췌, 라인별 설명, GPT 구조와 연결
주의사항: gist는 수정될 수 있으므로 실제 학습이나 재현 전에는 원본 revision을 다시 확인해야 합니다.


핵심 요약

  • microgpt.py는 GPT를 “라이브러리 호출”이 아니라 계산 그래프, attention, optimizer가 연결된 실행 루프로 보여줍니다.
  • PyTorch가 숨기는 Tensor, autograd, Module, optimizer.step()의 핵심 역할을 작은 Python 객체와 리스트로 직접 드러냅니다.
  • 모델은 대형 LLM이 아니라 이름 데이터셋을 문자 단위로 학습하는 작은 decoder-only Transformer입니다. 그래서 구조는 보이지만, 성능은 목적이 아닙니다.
  • attention은 Q, K, V, scaled dot product, softmax, weighted sum이라는 흐름으로 구현됩니다. 이 부분이 Transformer를 읽는 기준점입니다.
  • 이 코드를 읽고 나면 GPT 학습은 “다음 token 확률을 높이도록 계산 그래프를 만들고, gradient로 weight를 고치는 과정”으로 정리됩니다.

주의: 이 글의 코드 블록은 200라인 안팎의 원문 흐름을 따라 읽기 위한 발췌입니다. production용 GPT 구현이 아니라 학습용 reference를 해설하는 글입니다.


이 글을 읽는 순서

처음부터 끝까지 읽어도 되지만, 목적에 따라 보는 순서를 바꾸면 더 빠릅니다.

목표먼저 볼 구간
PyTorch autograd가 궁금하다Value 클래스, local gradient, backward()
Transformer attention이 궁금하다Q/K/V 생성, multi-head attention, KV cache
GPT 학습 루프가 궁금하다next-token 문제 변환, loss 계산, Adam update
추론이 궁금하다BOS 시작, temperature softmax, sampling

핵심 흐름만 보고 싶다면 Value.backward()gpt() → loss 계산 → Adam update → sampling 순서로 보면 됩니다.


1. microgpt.py는 어떤 코드인가

이 코드를 한 문장으로 정의하면 다음과 같습니다.

문자 단위 이름 데이터셋으로 작은 GPT를 학습하고,새로운 이름처럼 보이는 문자열을 생성하는 순수 Python 실행체

원본 파일은 os, math, random 같은 표준 라이브러리만 사용합니다. 코드에는 데이터 다운로드, tokenizer 구성, autograd 엔진, Transformer forward pass, Adam update, inference sampling이 순서대로 들어 있습니다. 그래서 이 파일은 “작은 모델”이라기보다 GPT 실행 경로를 관찰하기 위한 투명한 실험실에 가깝습니다.

항목내용
파일명microgpt.py
작성자Andrej Karpathy
구현 언어Python
외부 ML 의존성없음
데이터names.txt 형태의 이름 목록
tokenizer문자 단위 tokenizer
모델 구조작은 decoder-only Transformer
학습 목표다음 문자 예측
optimizer직접 구현한 Adam
추론 방식autoregressive sampling

핵심 메시지는 이렇습니다.

GPT의 중심은 다음 token 확률을 높이는 계산이다.다만 그 계산을 빠르고 크게 만들려면 별도의 시스템 엔지니어링이 필요하다.

2. 전체 실행 흐름

프로그램 흐름부터 잡고 가겠습니다.

input.txt 확인-> 없으면 names.txt 다운로드-> docs 리스트 생성-> 문자 vocabulary 생성-> Value 클래스로 scalar autograd 구성-> embedding / attention / MLP / lm_head weight 초기화-> 문서 하나씩 next-token prediction 학습-> loss.backward()-> Adam으로 parameter update-> BOS에서 시작해 문자 샘플 생성

이 흐름은 실제 GPT 학습의 축소판입니다.

실제 대형 모델에서는 데이터셋이 웹 규모이고, tokenizer가 BPE 계열이며, 연산은 GPU tensor kernel로 수행됩니다. 하지만 알고리즘의 중심은 여전히 같습니다.

토큰을 벡터로 바꾼다-> attention과 MLP를 통과시킨다-> 다음 토큰 확률을 예측한다-> 정답과 비교해 loss를 만든다-> gradient를 계산한다-> optimizer로 weight를 업데이트한다-> 추론 때는 다음 토큰을 하나씩 샘플링한다

3. import와 seed 고정

가장 먼저 볼 부분은 import와 seed입니다.

import os      # os.path.existsimport math    # math.log, math.expimport random  # seed, choices, gauss, shuffle random.seed(42)

이 네 줄은 단순해 보이지만 코드의 성격을 잘 보여줍니다.

코드역할
osinput.txt 파일 존재 여부를 확인합니다.
mathlog, exp 같은 수학 함수를 사용합니다.
random데이터 셔플, weight 초기화, 샘플링에 사용합니다.
random.seed(42)실행 결과를 어느 정도 재현 가능하게 만듭니다.

중요한 점은 numpy, torch, tensorflow가 없다는 것입니다. 즉 이 코드는 tensor library 없이도 GPT의 학습과 추론 흐름을 보여줍니다. 대신 모든 연산이 Python scalar와 list 위에서 일어나므로 속도는 매우 느립니다.

실무적으로는 이런 방식으로 학습하지 않습니다. 하지만 PyTorch가 내부에서 어떤 일을 대신해주는지 이해하기에는 좋은 형태입니다.


4. 데이터셋 준비

학습 데이터 준비는 대략 이런 모양입니다.

if not os.path.exists('input.txt'):    import urllib.request    names_url = 'https://raw.githubusercontent.com/karpathy/makemore/988aa59/names.txt'    urllib.request.urlretrieve(names_url, 'input.txt') docs = [line.strip() for line in open('input.txt') if line.strip()]random.shuffle(docs)print(f"num docs: {len(docs)}")

원본 코드는 input.txt가 없으면 makemorenames.txt를 내려받고, 각 줄을 하나의 document로 읽어 docs 리스트를 만듭니다.

단계설명
파일 확인현재 디렉터리에 학습 파일이 있는지 확인합니다.
원격 다운로드없으면 이름 데이터셋을 다운로드합니다.
줄 단위 읽기이름 하나를 document 하나로 봅니다.
빈 줄 제거의미 없는 입력을 제외합니다.
셔플학습 순서를 고정된 seed 아래에서 섞습니다.

docs는 대략 이런 형태입니다.

["emma", "olivia", "ava", "isabella"]

일반적인 GPT 학습에서 document라고 하면 웹페이지, 책, 코드 파일, 대화 기록 같은 긴 텍스트를 떠올릴 수 있습니다. 하지만 여기서는 이름 하나가 문서 하나입니다.

doc = "emma"

이 모델은 문장을 생성하는 모델이 아니라, 이름처럼 보이는 문자열을 생성하는 character-level GPT입니다.


5. 문자 단위 tokenizer

다음은 tokenizer입니다.

uchars = sorted(set(''.join(docs)))BOS = len(uchars)vocab_size = len(uchars) + 1 print(f"vocab size: {vocab_size}")

데이터셋에 등장하는 모든 고유 문자를 정렬하고, 마지막 index를 BOS 특수 토큰으로 사용합니다.

코드설명
unique_characters(docs)모든 이름에 등장한 문자를 모읍니다.
sorted(...)문자 순서를 고정합니다.
BOS = len(uchars)문자 index 다음 번호를 특수 토큰으로 둡니다.
vocab_size = len(uchars) + 1문자 수에 특수 토큰 1개를 더합니다.

예를 들어 데이터셋에 알파벳 소문자 26개만 있다면 다음과 비슷합니다.

uchars = ['a', 'b', 'c', ..., 'z']BOS = 26vocab_size = 27

실제 학습에서는 이름 앞뒤에 BOS를 붙입니다.

tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]

예를 들어 "emma"는 개념적으로 이렇게 변환됩니다.

[BOS, e, m, m, a, BOS]

첫 번째 BOS는 "이제 이름을 시작한다"는 의미입니다. 마지막 BOS는 "이름이 끝났다"는 의미로도 사용됩니다.

실무 tokenizer에서는 보통 BOS, EOS, PAD, UNK 등을 분리합니다. 이 코드는 단순성을 위해 하나의 특수 토큰을 시작과 종료 양쪽에 사용합니다.


6. Value 클래스와 scalar autograd

이 파일에서 가장 중요한 부분 중 하나는 Value 클래스입니다.

class Value:    __slots__ = ('data', 'grad', '_children', '_local_grads')     def __init__(self, data, children=(), local_grads=()):        self.data = data                  # forward 값        self.grad = 0                     # loss에 대한 gradient        self._children = children         # 계산 그래프의 이전 노드        self._local_grads = local_grads   # 각 child에 대한 local derivative

이 객체 하나는 단순한 숫자처럼 보이지만 실제로는 네 가지 정보를 갖습니다.

필드의미
dataforward pass에서 계산된 실제 숫자 값
gradloss가 이 값에 대해 얼마나 민감한지 나타내는 gradient
_children이 값을 만들 때 사용된 이전 노드들
_local_grads현재 노드가 child들에 대해 갖는 local derivative

PyTorch로 비유하면 Value는 아주 작은 Tensor입니다. 다만 PyTorch Tensor는 다차원 배열이고, Value는 scalar 하나입니다.

구분ValuePyTorch Tensor
단위scalar 하나다차원 배열
연산 방식Python scalar 연산vectorized tensor 연산
autograd직접 구현프레임워크 내장
속도느림빠름
목적교육용실전 학습/추론

Karpathy의 별도 프로젝트인 micrograd도 scalar 값 위에서 동적으로 DAG를 만들고 reverse-mode autodiff를 수행하는 작은 autograd engine입니다.

결국 microgpt.pyValue 클래스는 다음 질문에 답하기 위한 장치입니다.

loss.backward()가 마법이 아니라면,그 안에서는 정확히 무슨 일이 일어나는가?

7. 덧셈, 곱셈, local gradient

Value는 연산 결과를 새 Value로 만들면서 계산 그래프를 같이 저장합니다.

def __add__(self, other):    other = other if isinstance(other, Value) else Value(other)    return Value(self.data + other.data, (self, other), (1, 1)) def __mul__(self, other):    other = other if isinstance(other, Value) else Value(other)    return Value(self.data * other.data, (self, other), (other.data, self.data))

덧셈부터 보면 forward 값은 x + y이고, local gradient는 양쪽 모두 1입니다.

z = x + y dz/dx = 1dz/dy = 1

곱셈은 다릅니다.

z = x * y dz/dx = ydz/dy = x

그래서 곱셈의 local gradient는 상대방 값입니다.

이 구조 덕분에 나중에 loss.backward()를 호출하면 계산 그래프를 거꾸로 따라가면서 모든 parameter의 gradient를 구할 수 있습니다.


8. backward()와 chain rule

autograd의 핵심은 backward()입니다.

def backward(self):    topo = []    visited = set()     def build_topo(v):        if v not in visited:            visited.add(v)            for child in v._children:                build_topo(child)            topo.append(v)     build_topo(self)    self.grad = 1     for v in reversed(topo):        for child, local_grad in zip(v._children, v._local_grads):            child.grad += local_grad * v.grad

이 함수는 세 단계로 이해하면 됩니다.

1. loss에서 시작해 계산 그래프 전체를 수집한다.2. 그래프를 위상 정렬한다.3. 뒤에서 앞으로 돌면서 chain rule을 적용한다.

가장 중요한 줄은 이것입니다.

child.grad += local_grad * v.grad

수식으로 쓰면 다음과 같습니다.

dLoss/dChild += dCurrent/dChild * dLoss/dCurrent

즉 chain rule입니다. PyTorch의 다음 한 줄도 원리는 같습니다.

loss.backward()

차이는 실행 단위입니다. PyTorch는 tensor 단위로 빠르게 처리하고, microgpt.py는 scalar Value 객체 단위로 느리지만 투명하게 처리합니다.


9. 모델 파라미터 초기화

이제 GPT 모델이 학습할 parameter를 만듭니다.

n_layer = 1n_embd = 16block_size = 16n_head = 4head_dim = n_embd // n_head
설정의미
n_layer1Transformer block 개수
n_embd16token embedding 차원
block_size16최대 context 길이
n_head4attention head 수
head_dim4head 하나가 담당하는 차원

이 값들은 실제 LLM과 비교하면 극단적으로 작습니다. 하지만 구조는 GPT의 기본 형태를 유지합니다.

embedding-> attention-> MLP-> output logits

weight matrix도 일반 숫자가 아니라 Value 객체로 채웁니다.

matrix = lambda nout, nin, std=0.08: [    [Value(random.gauss(0, std)) for _ in range(nin)]    for _ in range(nout)]

이렇게 해야 forward pass 이후 각 weight가 자기 자신의 gradient를 가질 수 있습니다.


10. state_dict: GPT 구성 요소 모으기

파라미터는 state_dict에 저장됩니다.

state_dict = {    'wte': matrix(vocab_size, n_embd),    'wpe': matrix(block_size, n_embd),    'lm_head': matrix(vocab_size, n_embd),}
key역할
wtetoken embedding
wpeposition embedding
lm_headhidden vector를 vocab logits로 바꾸는 출력층

Transformer layer 내부 weight는 다음 성격을 갖습니다.

weight의미
attn_wqquery projection
attn_wkkey projection
attn_wvvalue projection
attn_woattention output projection
mlp_fc1MLP 확장 projection
mlp_fc2MLP 축소 projection

MLP는 n_embd를 4배로 키웠다가 다시 줄입니다.

16 -> 64 -> 16

마지막으로 모든 parameter를 1차원 리스트로 펼칩니다. optimizer가 모든 parameter를 순회하면서 업데이트하기 쉽게 만들기 위해서입니다.


11. linear, softmax, rmsnorm

모델 forward에서 반복적으로 쓰이는 helper는 세 개입니다.

def linear(x, w):    return [dot(row, x) for row in w]

이 함수는 행렬-벡터 곱입니다. PyTorch로 쓰면 대략 다음과 같습니다.

y = W @ x

하지만 여기서는 tensor 연산이 아닙니다. 모든 곱셈과 덧셈은 Value 연산입니다. 따라서 linear()는 단순 계산이면서 동시에 autograd graph를 만듭니다.

다음은 softmax입니다.

def softmax(logits):    max_val = max(val.data for val in logits)    exps = [(val - max_val).exp() for val in logits]    total = sum(exps)    return [e / total for e in exps]

softmax는 logits를 확률 분포로 바꿉니다.

softmax(x_i) = exp(x_i) / sum(exp(x_j))

여기서 최대값을 먼저 빼는 이유는 numerical stability 때문입니다. 예를 들어 logits가 [1000, 1001, 1002]라면 그대로 지수 함수를 적용할 때 overflow가 날 수 있습니다. 최대값을 빼면 [-2, -1, 0]이 되고, softmax 결과는 동일하지만 계산은 훨씬 안정적입니다.

다음은 RMSNorm입니다.

def rmsnorm(x):    ms = sum(xi * xi for xi in x) / len(x)    scale = (ms + 1e-5) ** -0.5    return [xi * scale for xi in x]

RMSNorm은 입력 벡터의 root mean square를 기준으로 값을 정규화합니다. RMSNorm 논문은 LayerNorm에서 평균을 빼는 re-centering을 제거하고 RMS 통계 기반 정규화를 제안합니다.


12. gpt() 함수와 embedding

이제 모델 본체인 gpt() 함수입니다.

def gpt(token_id, pos_id, keys, values):    tok_emb = state_dict['wte'][token_id]    pos_emb = state_dict['wpe'][pos_id]    x = [t + p for t, p in zip(tok_emb, pos_emb)]    x = rmsnorm(x)
코드의미
wte[token_id]현재 token의 embedding vector를 가져옵니다.
wpe[pos_id]현재 위치의 position embedding을 가져옵니다.
tok_emb + pos_embtoken 정보와 위치 정보를 더합니다.
rmsnorm(x)입력 벡터 크기를 정규화합니다.

Transformer는 token id 자체를 바로 처리하지 않습니다. 먼저 token id를 vector로 바꿉니다.

token_id -> token embedding vector

그런데 attention만으로는 token의 순서를 알 수 없습니다. 그래서 위치 정보도 같이 넣어야 합니다.

token embedding + position embedding

Transformer 논문도 recurrence와 convolution이 없는 구조에서는 sequence 순서 정보를 사용하기 위해 positional encoding을 input embedding에 더한다고 설명합니다.


13. Q, K, V 만들기

Transformer block 안으로 들어가면 먼저 attention을 준비합니다.

x_residual = xx = rmsnorm(x) q = linear(x, state_dict[f'layer{li}.attn_wq'])k = linear(x, state_dict[f'layer{li}.attn_wk'])v = linear(x, state_dict[f'layer{li}.attn_wv']) keys[li].append(k)values[li].append(v)

Q, K, V는 attention의 핵심 개념입니다.

이름의미직관
Query현재 token이 찾고 싶은 정보"나는 무엇을 보고 싶은가?"
Key각 token이 가진 주소 또는 특징"나는 어떤 정보인가?"
Value실제로 전달될 내용"내가 줄 내용은 무엇인가?"

keys[layer].append(k)values[layer].append(v)는 작은 KV cache입니다. 현재 위치까지의 key/value만 쌓이기 때문에 모델은 미래 token을 볼 수 없습니다.

일반적인 decoder-only Transformer에서는 causal mask를 사용해 미래 token을 가립니다. microgpt.py는 sequence 전체를 한 번에 넣는 방식이 아니라 왼쪽에서 오른쪽으로 token을 하나씩 처리합니다. 그래서 명시적인 causal mask 없이도 autoregressive 조건이 유지됩니다.

현재 position에서 볼 수 있는 것:[BOS, 이전 문자들, 현재 문자] 현재 position에서 볼 수 없는 것:미래 문자들

14. Multi-head attention 해체

n_embd = 16, n_head = 4라면 head 하나의 차원은 4입니다.

전체 embedding 16차원 head 0: 0~3head 1: 4~7head 2: 8~11head 3: 12~15

하나의 큰 attention을 수행하는 대신 vector를 여러 조각으로 나누어 여러 관점에서 attention을 수행합니다.

for h in range(n_head):    hs = h * head_dim    q_h = q[hs:hs+head_dim]    k_h = [ki[hs:hs+head_dim] for ki in keys[li]]    v_h = [vi[hs:hs+head_dim] for vi in values[li]]

attention score는 scaled dot-product로 계산합니다.

attn_logits = [    sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5    for t in range(len(k_h))]attn_weights = softmax(attn_logits)

Transformer 논문은 query와 key의 dot product를 key 차원의 제곱근으로 나눈 뒤 softmax를 적용해 value에 대한 weight를 만든다고 설명합니다.

그 다음 value를 가중합합니다.

head_out = [    sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h)))    for j in range(head_dim)]x_attn.extend(head_out)

즉 attention은 다음 질문에 답합니다.

현재 token이 다음 token을 예측하려면,이전 token들 중 무엇을 얼마나 참고해야 하는가?

15. Attention output과 residual connection

각 head의 결과를 이어붙인 뒤에는 output projection을 수행합니다.

x = linear(x_attn, state_dict[f'layer{li}.attn_wo'])x = x + x_residual

첫 줄은 여러 head의 출력을 다시 embedding 차원으로 섞습니다.

multi-head output -> output projection

두 번째 줄은 residual connection입니다.

x = attention_output + original_input

Residual connection은 깊은 네트워크에서 정보와 gradient가 잘 흐르게 도와줍니다. Transformer 논문도 각 sub-layer 주변에 residual connection을 사용합니다.

microgpt.py에서는 layer가 1개뿐이지만 구조 자체는 GPT block의 기본 패턴을 따릅니다.

RMSNorm-> Multi-head Attention-> Output Projection-> Residual Add

16. MLP block과 lm_head

Attention 다음에는 MLP block이 옵니다.

x_residual = xx = rmsnorm(x) x = linear(x, state_dict[f'layer{li}.mlp_fc1'])x = [xi.relu() for xi in x]x = linear(x, state_dict[f'layer{li}.mlp_fc2']) x = x + x_residual

구조를 한 줄로 쓰면 이렇습니다.

x -> RMSNorm -> Linear -> ReLU -> Linear -> Residual Add

크기로 보면 다음과 같습니다.

16 -> 64 -> 16

Attention이 "어떤 과거 token을 볼지"를 정한다면, MLP는 각 위치의 hidden representation을 비선형적으로 변환합니다. Transformer 논문도 attention sub-layer 외에 position-wise feed-forward network를 둡니다.

마지막으로 output head를 통과합니다.

logits = linear(x, state_dict['lm_head'])return logits

logits는 아직 확률이 아닙니다.

logits = 다음 token 후보들에 대한 원시 점수

예를 들어 vocab_size = 27이라면 logits도 27개입니다. 여기에 softmax를 적용하면 다음 token 확률이 됩니다.

probs = softmax(logits)

결국 gpt() 함수는 다음 역할을 합니다.

현재 token, 현재 position, 과거 KV cache를 받아다음 token에 대한 logits를 반환한다.

17. 학습 루프: 이름 하나를 next-token 문제로 바꾸기

학습 루프는 각 step에서 문서 하나를 고르고, 앞뒤에 BOS를 붙인 token sequence를 만듭니다.

for step in range(num_steps):    doc = docs[step % len(docs)]    tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]    n = min(block_size, len(tokens) - 1)
코드의미
docs[step % len(docs)]문서를 하나 선택합니다.
encode(doc)문자를 token id로 변환합니다.
[BOS] + ... + [BOS]시작과 종료 토큰을 붙입니다.
min(block_size, ...)최대 context 길이를 넘지 않게 자릅니다.

예를 들어 doc = "emma"라면 다음 sequence가 됩니다.

[BOS, e, m, m, a, BOS]

이 sequence는 학습 중 다음 문제들로 바뀝니다.

입력 token정답 token
BOSe
em
mm
ma
aBOS

GPT 학습의 기본 목표는 이것입니다.

지금까지 본 token들을 바탕으로 다음 token을 맞혀라.

18. loss 계산

forward pass와 loss 계산은 다음 구조입니다.

keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]losses = [] for pos_id in range(n):    token_id, target_id = tokens[pos_id], tokens[pos_id + 1]    logits = gpt(token_id, pos_id, keys, values)    probs = softmax(logits)    loss_t = -probs[target_id].log()    losses.append(loss_t) loss = (1 / n) * sum(losses)

핵심은 정답 token 확률의 negative log likelihood입니다.

loss = -log(P(correct_next_token))

정답 token 확률이 높으면 loss는 작아집니다.

P(correct) = 0.9-log(0.9) ~= 0.105

정답 token 확률이 낮으면 loss는 커집니다.

P(correct) = 0.01-log(0.01) ~= 4.605

즉 모델은 실제 다음 문자에 더 높은 확률을 주도록 weight를 바꾸는 방향으로 학습됩니다.


19. loss.backward(): 모든 parameter로 gradient 흘려보내기

loss가 만들어지면 역전파를 실행합니다.

loss.backward()

이 한 줄은 앞에서 만든 Value.backward()를 호출합니다. 실제로는 다음 경로를 거꾸로 따라갑니다.

loss-> log-> softmax-> logits-> lm_head-> MLP-> attention-> embedding-> parameters

각 parameter는 Value 객체입니다. 따라서 loss.backward()가 끝나면 각 parameter의 grad 필드가 채워집니다.

p.grad

이 값은 다음 의미를 가집니다.

이 parameter를 조금 바꾸면 loss가 얼마나 변하는가?

실무적으로 PyTorch의 loss.backward()를 이해할 때도 이 관점이 중요합니다. 이 한 줄은 마법이 아니라, 계산 그래프를 거꾸로 따라가며 chain rule을 적용하는 과정입니다.


20. Adam optimizer 직접 구현

이제 gradient를 이용해 parameter를 업데이트합니다.

learning_rate = 0.01beta1 = 0.85beta2 = 0.99eps_adam = 1e-8 m = [0.0] * len(params)v = [0.0] * len(params)

Adam 논문은 Adam을 gradient의 lower-order moments에 대한 adaptive estimates를 사용하는 stochastic optimization 알고리즘으로 소개합니다.

변수의미
learning_rateparameter를 한 번에 얼마나 움직일지 결정합니다.
beta1gradient 이동 평균의 decay 계수입니다.
beta2gradient 제곱 이동 평균의 decay 계수입니다.
eps0으로 나누는 것을 방지합니다.
mfirst moment buffer입니다.
vsecond moment buffer입니다.

업데이트는 다음 절차로 진행됩니다.

1. step이 진행될수록 learning rate를 줄인다.2. gradient의 이동 평균 m을 업데이트한다.3. gradient 제곱의 이동 평균 v를 업데이트한다.4. 초기 step에서 생기는 bias를 보정한다.5. parameter 값을 직접 수정한다.6. gradient를 0으로 초기화한다.

원문 update loop의 핵심은 이 부분입니다.

lr_t = learning_rate * (1 - step / num_steps) for i, p in enumerate(params):    m[i] = beta1 * m[i] + (1 - beta1) * p.grad    v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2     m_hat = m[i] / (1 - beta1 ** (step + 1))    v_hat = v[i] / (1 - beta2 ** (step + 1))     p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam)    p.grad = 0

마지막 줄은 특히 중요합니다.

p.grad = 0

gradient를 초기화하지 않으면 다음 step의 gradient가 이전 step에 누적됩니다. PyTorch로 치면 다음 코드와 비슷한 역할입니다.

optimizer.zero_grad()optimizer.step()

21. 추론: BOS에서 시작해 한 글자씩 생성하기

학습이 끝나면 새로운 이름을 생성합니다.

temperature = 0.5 for sample_idx in range(20):    keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]    token_id = BOS    sample = []

생성 루프는 다음과 같습니다.

for pos_id in range(block_size):    logits = gpt(token_id, pos_id, keys, values)    probs = softmax([l / temperature for l in logits])     token_id = random.choices(        range(vocab_size),        weights=[p.data for p in probs]    )[0]     if token_id == BOS:        break     sample.append(uchars[token_id])

생성 흐름은 다음과 같습니다.

BOS-> 첫 글자 샘플링-> 두 번째 글자 샘플링-> ...-> BOS가 나오면 종료

temperature는 생성의 무작위성을 조절합니다.

temperature효과
낮음높은 확률 token을 더 강하게 선호합니다. 결과가 안정적이지만 단조로울 수 있습니다.
높음낮은 확률 token도 선택될 가능성이 커집니다. 결과가 다양하지만 이상해질 수 있습니다.

여기서는 temperature = 0.5입니다. 완전히 무작위로 생성하기보다는, 모델이 그럴듯하다고 판단한 문자를 더 강하게 선호합니다.


22. 전체 구조를 한 번에 다시 보기

microgpt.py의 전체 구조를 다시 요약하면 이렇습니다.

구간코드 요소GPT 관점의 의미
데이터 준비docs학습 문서 목록
tokenizeruchars, BOS, vocab_size문자열을 token id로 변환
autogradValuescalar 기반 계산 그래프와 gradient
parameterstate_dict모델이 학습할 weight
embeddingwte, wpetoken과 position을 vector로 변환
attentionQ/K/V projection과거 token을 참고
MLPmlp_fc1, mlp_fc2hidden representation 변환
outputlm_head다음 token logits 생성
loss-log(prob[target])정답 token 확률을 높이는 목적 함수
backwardloss.backward()gradient 계산
optimizerAdam updateparameter 수정
inference확률 샘플링다음 token 생성

핵심을 한 문장으로 줄이면 이렇습니다.

microgpt.py는 GPT의 학습과 추론을 PyTorch 없이 손으로 추적할 수 있게 만든 실행 가능한 해부도다.

실제 LLM과의 차이도 분명히 해야 합니다.

항목microgpt.py실제 LLM
tokenizer문자 단위BPE, SentencePiece, tiktoken 등
연산 단위scalar Valuetensor
학습 데이터이름 목록대규모 corpus
모델 크기수천 parameter 수준수십억에서 수조 parameter 가능
학습 방식문서 하나씩 순차 처리batch, distributed training
가속없음GPU/TPU, fused kernel
목적교육용production 또는 research

23. 이 코드에서 특히 배울 만한 것

이 코드를 단순히 "작은 GPT 구현"으로만 보면 아쉽습니다. 더 중요한 학습 포인트는 세 가지입니다. 이 세 가지가 잡히면, PyTorch로 작성된 Transformer 코드도 훨씬 덜 불투명해집니다.

23.1 Autograd는 계산 그래프와 chain rule이다

Value 클래스는 PyTorch autograd의 축소판입니다.

forward 때 graph 생성-> backward 때 chain rule 적용-> parameter.grad 채우기

이 과정을 직접 보면 다음 코드가 훨씬 명확해집니다.

loss.backward()optimizer.step()optimizer.zero_grad()

23.2 Attention은 Q와 K의 비교, V의 가중합이다

attention의 핵심은 세 줄입니다.

score = q dot k / sqrt(d)weight = softmax(score)output = sum(weight * value)

즉 현재 token은 과거 token들을 모두 똑같이 보는 것이 아니라, 필요한 token에 더 높은 weight를 줍니다.

23.3 GPT 학습은 다음 token 확률을 높이는 과정이다

학습 목표는 복잡하지 않습니다.

현재까지 본 token들로 다음 token을 맞힌다.

loss는 정답 token 확률의 negative log likelihood입니다.

loss = -log(P(correct_next_token))

이 원리는 작은 이름 생성 모델에서도, 대형 언어 모델에서도 중심 아이디어로 남아 있습니다.


24. 실전 체크리스트

원본 코드를 직접 따라갈 때는 아래 순서로 보면 좋습니다.

microgpt.py 읽기 체크리스트 [ ] 원본 gist 기준일을 확인했다.[ ] input.txt가 없을 때 names.txt를 내려받는 흐름을 이해했다.[ ] docs 하나가 이름 하나라는 점을 이해했다.[ ] uchars, BOS, vocab_size가 tokenizer 역할을 한다는 점을 이해했다.[ ] Value.data와 Value.grad의 차이를 설명할 수 있다.[ ] add, multiply가 local gradient를 저장하는 방식을 이해했다.[ ] backward()의 topological sort와 chain rule을 설명할 수 있다.[ ] wte, wpe, lm_head의 역할을 구분할 수 있다.[ ] Q, K, V projection의 역할을 설명할 수 있다.[ ] attention score = q dot k / sqrt(head_dim)을 이해했다.[ ] keys, values list가 작은 KV cache 역할을 한다는 점을 이해했다.[ ] MLP block이 16 -> 64 -> 16 구조라는 점을 이해했다.[ ] loss = -log(prob[target])의 의미를 설명할 수 있다.[ ] Adam의 m, v, bias correction을 대략 설명할 수 있다.[ ] inference에서 temperature가 하는 일을 이해했다.

25. Q&A

Q1. 이 코드는 진짜 GPT인가?

구조적으로는 decoder-only Transformer 기반 next-token model입니다. 다만 크기가 매우 작고, tokenizer도 문자 단위이며, 학습 데이터도 이름 목록입니다.

그래서 실전 GPT 구현이라기보다는 GPT 구조를 이해하기 위한 교육용 미니어처라고 보는 것이 정확합니다.

Q2. 왜 PyTorch를 쓰지 않았나?

목적이 성능이 아니라 알고리즘 설명이기 때문입니다.

PyTorch를 사용하면 Tensor, Module, autograd, optimizer가 많은 일을 감춰줍니다. 실무에서는 그게 장점입니다. 하지만 학습 목적에서는 오히려 내부 구조가 보이지 않을 수 있습니다.

microgpt.py는 그 감춰진 부분을 일부러 드러냅니다.

Q3. 왜 BOS를 끝 token으로도 쓰나?

단순화 때문입니다.

실제 tokenizer에서는 보통 BOSEOS를 분리합니다. 하지만 이 코드는 이름 생성이라는 작은 문제를 다루므로, 하나의 특수 token을 시작과 종료에 모두 사용합니다.

Q4. causal mask가 없는데 미래 token을 보지 않나?

이 코드는 sequence 전체를 한 번에 처리하지 않습니다. 각 token을 왼쪽에서 오른쪽으로 하나씩 처리하고, 현재까지의 key/value만 cache에 쌓습니다.

따라서 현재 position에서는 미래 token의 key/value가 아직 존재하지 않습니다.

미래를 mask로 가리는 대신,아예 미래를 계산하지 않은 상태에서 진행한다.

Q5. 이 코드를 그대로 키우면 LLM이 되나?

개념적으로는 방향이 맞지만, 그대로는 어렵습니다. 실제 LLM에는 다음 요소들이 필요합니다.

대규모 tokenizer대규모 dataset pipelinebatch trainingtensor 연산GPU/TPU 가속mixed precisiondistributed trainingcheckpointevaluationserving stack보안 및 비용 관리

microgpt.py는 알고리즘의 뼈대를 보여주는 코드입니다. 실전 LLM은 그 위에 거대한 시스템 엔지니어링이 붙습니다.

Q6. 이 코드에서 가장 중요한 줄은 무엇인가?

개인적으로는 세 가지 흐름입니다.

child.grad += local_grad * node.grad

이 줄은 autograd의 핵심인 chain rule입니다.

attention_score = q dot k / sqrt(head_dim)

이 식은 attention의 핵심입니다.

loss = -log(P(correct_next_token))

이 식은 next-token prediction 학습 목표를 보여줍니다.


26. 참고자료와 불확실성

참고자료

확인된 사실

  • 2026-05-05 확인 기준 원본 gist에는 microgpt.py 파일이 포함되어 있고, GitHub Gist 페이지에는 Last active May 5, 2026 01:01로 표시됩니다.
  • 원본 코드에는 데이터 로딩, tokenizer, Value autograd, Transformer forward, Adam 학습 루프, inference sampling이 포함되어 있습니다.
  • 원본 코드 주석은 이 모델이 GPT-2를 따르되 LayerNorm 대신 RMSNorm, bias 없음, GeLU 대신 ReLU를 사용한다고 설명합니다.

작성자의 해석

  • 이 코드는 GPT를 실제로 서비스하기 위한 구현이 아니라, GPT 알고리즘을 손으로 따라가기 위한 교육용 reference에 가깝습니다.
  • Value 클래스는 PyTorch autograd를 이해하는 데 좋은 출발점입니다.
  • Attention 구현은 tensor operation이 아니라 Python list와 scalar 연산으로 되어 있어 느리지만 구조를 읽기 좋습니다.

불확실성

  • gist는 수정될 수 있으므로 실제 학습이나 재현 전에는 원본 revision을 다시 확인해야 합니다.
  • 실행 시간은 Python 버전, CPU 성능, 로컬 환경에 따라 달라질 수 있습니다.
  • 이 글의 해설은 2026-05-05에 확인한 원본 gist 구조를 기준으로 합니다.

마무리

정리하면, microgpt.py는 GPT를 작게 만든 코드라기보다 GPT를 이해하기 위해 불필요한 장치를 걷어낸 코드에 가깝습니다.

실제 LLM을 만들려면 훨씬 더 많은 시스템이 필요합니다. tokenizer도 달라지고, 학습 데이터도 커지고, batch training, GPU kernel, distributed training, checkpoint, serving stack이 붙습니다. 하지만 GPT의 중심 흐름은 이 파일 안에 거의 다 들어 있습니다.

문자 -> tokentoken -> embeddingembedding -> attentionattention -> MLPMLP -> logitslogits -> lossloss -> gradientgradient -> Adam updateBOS -> sampling -> generated text

처음 읽을 때는 gpt() 함수보다 Value.backward()를 먼저 보는 것이 좋습니다. 이유는 단순합니다. 이 파일의 가장 큰 가치는 “Transformer를 구현했다”가 아니라, forward 계산이 어떻게 graph가 되고, 그 graph가 어떻게 gradient로 돌아오는지를 보여준다는 데 있습니다.

그 다음 linear(), softmax(), attention score 계산, loss 계산 순서로 따라가면 전체 구조가 선명해집니다. 이 코드를 이해했다면 PyTorch로 작성된 Transformer 코드도 더 이상 loss.backward()optimizer.step()이라는 두 줄의 마법처럼 보이지 않을 것입니다.

When you open the code to properly understand GPT, surprisingly, the peripherals come out before the essence.

PyTorch, CUDA, tokenizer, dataset loader, optimizer, scheduler, checkpoint, distributed training. In practice, this is a necessary device. But when you first try to figure out “what does GPT actually calculate,” these devices obscure your view.

The reason Andrej Karpathy'smicrogpt.pyis good isn't because it's small. This is because it peels off almost all hidden layers and makes the GPT learning loop visible all the way through in one file. The path starting from the character tokenizer to scalar autograd, Q/K/V attention, negative log likelihood, Adam update, and autoregressive sampling is revealed.

The goal of this article is not to “introduce a small GPT implementation”. The goal is to check what calculations occur behindloss.backward()andoptimizer.step(), why attention is divided into Q/K/V, and what kind of loop the next token learning turns into in the actual code.

microgpt.pyis a short file of around 200 lines. So, rather than avoiding the original text and explaining it, read this article following the flow of the original text, including enough key code blocks and dismantling them right below. It's a safe bet to check back at Karpathy's microgpt.py gist for the most up-to-date source at the time of publication.

Analysis base date: 2026-05-05
Analysis target: Andrej Karpathymicrogpt.pygist
Analysis method: Excerpt of key code blocks following the flow of the original text, explanation by line, connection to GPT structure
Caution: Since the gist can be modified, you must re-check the original revision before actual study or reproduction.


Key takeaways

  • microgpt.pyshows GPT not as a “library call” but as an execution loop with a computational graph, attention, and optimizer connected.
  • The key roles ofTensor,autograd,Module, andoptimizer.step()hidden by PyTorch are directly revealed as small Python objects and lists.
  • The model is not a large LLM, but a small decoder-only Transformer that learns the name dataset character by character. So the structure is visible, but performance is not the goal.
  • Attention is implemented in the following flows:Q,K,V, scaled dot product, softmax, and weighted sum. This part is the reference point for reading Transformer.
  • After reading this code, GPT learning can be summarized as “the process of creating a computational graph to increase the probability of the next token and modifying the weight with gradient.”

Note: The code blocks in this article are excerpts to be read along the flow of the original text, which is about 200 lines long. This is not an implementation of GPT for production, but rather a reference for learning.


Order of reading this article

You can read it from beginning to end, but it's faster if you change the viewing order depending on your purpose.

targetSection to see first
I am curious about PyTorch autograd.Valueclass, local gradient,backward()
I’m curious about Transformer attentionQ/K/V generation, multi-head attention, KV cache
I am curious about the GPT learning loop.Next-token problem conversion, loss calculation, Adam update
I'm curious about the reasoning.BOS start, temperature softmax, sampling

If you want to see only the core flow, you can look in the following order:Value.backward()togpt()to loss calculation to Adam update to sampling.


1. What code ismicrogpt.py?

This code can be defined in one sentence as follows:

Learn a small GPT with a character-level name dataset,A pure Python executable that generates a string that looks like a new name.

The original file only uses standard libraries such asos,math, andrandom. The code includes data download, tokenizer configuration, autograd engine, Transformer forward pass, Adam update, and inference sampling, in that order. So this file is less of a “small model” and more of a transparent laboratory for observing GPT execution paths.

itemdetail
file namemicrogpt.py
AuthorAndrej Karpathy
implementation languagePython
External ML dependenciesdoesn't exist
dataList of names in the formnames.txt
tokenizerCharacter-by-character tokenizer
model structureSmall decoder-only Transformer
learning objectivesNext character prediction
optimizerAdam implemented it himself
way of reasoningautoregressive sampling

The core message is this:

The center of GPT is the calculation that increases the probability of the next token.However, separate system engineering is required to make the calculation quickly and large.

2. Entire execution flow

Let's start with the flow of the program.

Check input.txtIf you don't have to, download names.txtCreate a to docs listto create character vocabularyConfigure scalar autograd with to Value classto embed / attention / MLP / lm_head weight initializationTo learn next-token prediction one by one-> loss.backward()parameter update to Adamto Generate character samples starting from BOS

This flow is a microcosm of actual GPT training.

In an actual large model, the dataset is web-scale, the tokenizer is a BPE series, and the computation is performed with a GPU tensor kernel. But the core of the algorithm is still the same.

Convert tokens to vectorsto attention and pass MLPto predict the next token probabilityto create a loss by comparing it with the correct answer.Calculate the to gradientUpdate weight with to optimizerDuring to inference, the next tokens are sampled one by one.

3. Import and seed fixation

The first part to look at is import and seed.

import os      # os.path.existsimport math    # math.log, math.expimport random  # seed, choices, gauss, shuffle random.seed(42)

These four lines may seem simple, but they show the nature of the code.

cordrole
osCheck whether theinput.txtfile exists.
mathUse mathematical functions such aslogandexp.
randomUsed for data shuffling, weight initialization, and sampling.
random.seed(42)Makes the execution results somewhat reproducible.

The important thing is that there is nonumpy,torch,tensorflow. In other words, this code shows the learning and inference flow of GPT without the tensor library. Instead, all operations occur on Python scalars and lists, so the speed is very slow.

In practice, we don't learn this way. But it's a good form to understand what PyTorch does under the hood.


4. Dataset preparation

Preparing training data looks something like this:

if not os.path.exists('input.txt'):    import urllib.request    names_url = 'https://raw.githubusercontent.com/karpathy/makemore/988aa59/names.txt'    urllib.request.urlretrieve(names_url, 'input.txt') docs = [line.strip() for line in open('input.txt') if line.strip()]random.shuffle(docs)print(f"num docs: {len(docs)}")

The original code downloadsnames.txtofmakemoreif there is noinput.txt, reads each line as one document, and creates adocslist.

stepexplanation
check fileCheck whether the training file exists in the current directory.
remote downloadIf not, download the name dataset.
Read line by lineOne name is considered one document.
Remove blank linesExclude meaningless input.
shuffleShuffle the training sequence under a fixed seed.

docslooks roughly like this.

["emma", "olivia", "ava", "isabella"]

In general GPT learning, when you think of a document, you might think of a long text such as a web page, book, code file, or conversation record. But here, one name is one document.

doc = "emma"

This model is not a model that generates sentences, but a character-level GPT that generates strings that look like names.


5. Character-level tokenizer

Next is the tokenizer.

uchars = sorted(set(''.join(docs)))BOS = len(uchars)vocab_size = len(uchars) + 1 print(f"vocab size: {vocab_size}")

Sort all unique characters that appear in the dataset, and use the last index as theBOSspecial token.

cordexplanation
unique_characters(docs)Collect the letters that appear in all names.
sorted(...)Fixes the character order.
BOS = len(uchars)Let the number following the character index be a special token.
vocab_size = len(uchars) + 1Add 1 special token to the number of characters.

For example, if your dataset only has 26 lowercase letters of the alphabet, it would look like this:

uchars = ['a', 'b', 'c',..., 'z']BOS = 26vocab_size = 27

In actual learning, addBOSbefore and after the name.

tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]

For example,"emma"is conceptually converted to this:

[BOS, e, m, m, a, BOS]

The firstBOSmeans "Now the name begins". The finalBOSis also used to mean "the end of the name."

In practice, tokenizers usually separateBOS,EOS,PAD,UNK, etc. This code uses one special token for both start and end for simplicity.


6.Valueclass and scalar autograd

One of the most important parts of this file is theValueclass.

class Value:    __slots__ = ('data', 'grad', '_children', '_local_grads')     def __init__(self, data, children=(), local_grads=()):        self.data = data                  # forward value        self.grad = 0                     # gradient for loss        self._children = children         # Previous node in the computation graph        self._local_grads = local_grads   # local derivative for each child

This one object looks like a simple number, but it actually has four pieces of information.

fieldmeaning
dataActual numeric value calculated in forward pass
gradgradient indicating how sensitive the loss is to this value
_childrenPrevious nodes used to create this value
_local_gradsThe local derivative of the current node with respect to its children

By analogy to PyTorch,Valueis a very smallTensor. However, PyTorch Tensor is a multidimensional array, andValueis a single scalar.

divisionValuePyTorch Tensor
unitscalar onemultidimensional array
algorithmPython scalar operationsvectorized tensor operations
autogradimplement it yourselfBuilt-in framework
speedslowspeed
purposeFor educational usePractical learning/reasoning

Karpathy's separate project, micrograd, is also a small autograd engine that dynamically creates a DAG on scalar values ​​and performs reverse-mode autodiff.

Ultimately, theValueclass ofmicrogpt.pyis a device for answering the following questions:

If loss.backward() isn't magic,What exactly happens inside?

7. Addition, multiplication, local gradient

Valuesaves the calculation graph while creating the calculation result as a newValue.

def __add__(self, other):    other = other if isinstance(other, Value) else Value(other)    return Value(self.data + other.data, (self, other), (1, 1)) def __mul__(self, other):    other = other if isinstance(other, Value) else Value(other)    return Value(self.data * other.data, (self, other), (other.data, self.data))

Starting with addition, the forward value isx + y, and the local gradient is 1 on both sides.

z = x + y dz/dx = 1dz/dy = 1

Multiplication is different.

z = x * y dz/dx = ydz/dy = x

So the local gradient of multiplication is the counterpart value.

Thanks to this structure, when you later callloss.backward(), you can obtain the gradient of all parameters by following the calculation graph backwards.


8.backward()and chain rule

The core of autograd isbackward().

def backward(self):    topo = []    visited = set()     def build_topo(v):        if v not in visited:            visited.add(v)            for child in v._children:                build_topo(child)            topo.append(v)     build_topo(self)    self.grad = 1     for v in reversed(topo):        for child, local_grad in zip(v._children, v._local_grads):            child.grad += local_grad * v.grad

This function can be understood in three steps.

1. Starting from loss, collect the entire computational graph.2. Topologically sort the graph.3. Apply the chain rule by rotating from back to front.

The most important line is this:

child.grad += local_grad * v.grad

When written as a formula, it is as follows:

dLoss/dChild += dCurrent/dChild * dLoss/dCurrent

In other words, it is a chain rule. The principle behind the next line in PyTorch is the same.

loss.backward()

The difference is the execution unit. PyTorch processes quickly in tensor units, andmicrogpt.pyprocesses slowly but transparently in units of scalarValueobjects.


9. Initialize model parameters

Now we create parameters for the GPT model to learn.

n_layer = 1n_embd = 16block_size = 16n_head = 4head_dim = n_embd // n_head
settingvaluemeaning
n_layer1Transformer block number
n_embd16token embedding dimension
block_size16Maximum context length
n_head4number of attention heads
head_dim4The dimension that one head is responsible for

These values ​​are extremely small compared to actual LLM. However, the structure retains the basic form of GPT.

embedding-> attention-> MLP-> output logits

The weight matrix is ​​also filled withValueobjects rather than regular numbers.

matrix = lambda nout, nin, std=0.08: [    [Value(random.gauss(0, std)) for _ in range(nin)]    for _ in range(nout)]

This way, each weight can have its own gradient after the forward pass.


10.state_dict: Gather GPT components

The parameters are stored instate_dict.

state_dict = {    'wte': matrix(vocab_size, n_embd),    'wpe': matrix(block_size, n_embd),    'lm_head': matrix(vocab_size, n_embd),}
keyrole
wtetoken embedding
wpeposition embedding
lm_headOutput layer that converts hidden vectors into vocab logits

The weights inside the Transformer layer have the following characteristics.

weightmeaning
attn_wqquery projection
attn_wkkey projection
attn_wvvalue projection
attn_woattention output projection
mlp_fc1MLP extended projection
mlp_fc2MLP reduced projection

MLP quadruplesn_embdand then decreases it again.

16 -> 64 -> 16

Finally, expand all parameters into a one-dimensional list. This is to make it easier for the optimizer to traverse and update all parameters.


11.linear,softmax,rmsnorm

There are three helpers that are repeatedly used in the model forward.

def linear(x, w):    return [dot(row, x) for row in w]

This function is a matrix-vector product. When written in PyTorch, it looks roughly like this:

y = W @ x

But here it is not a tensor operation. All multiplication and addition areValueoperations. Therefore,linear()is a simple calculation and at the same time creates an autograd graph.

Next is softmax.

def softmax(logits):    max_val = max(val.data for val in logits)    exps = [(val - max_val).exp() for val in logits]    total = sum(exps)    return [e / total for e in exps]

softmax turns logits into a probability distribution.

softmax(x_i) = exp(x_i) / sum(exp(x_j))

The reason for subtracting the maximum value first here is because of numerical stability. For example, if logits is[1000, 1001, 1002], overflow may occur when applying the exponential function as is. Subtracting the maximum gives[-2, -1, 0], the softmax result is the same, but the calculation is much more stable.

Next is RMSNorm.

def rmsnorm(x):    ms = sum(xi * xi for xi in x) / len(x)    scale = (ms + 1e-5) ** -0.5    return [xi * scale for xi in x]

RMSNorm normalizes values ​​based on the root mean square of the input vector. RMSNorm paper removes the mean-subtracting re-centering in LayerNorm and proposes RMS statistics-based normalization.


12.gpt()function and embedding

Now this is the model body, thegpt()function.

def gpt(token_id, pos_id, keys, values):    tok_emb = state_dict['wte'][token_id]    pos_emb = state_dict['wpe'][pos_id]    x = [t + p for t, p in zip(tok_emb, pos_emb)]    x = rmsnorm(x)
cordmeaning
wte[token_id]Get the embedding vector of the current token.
wpe[pos_id]Get the position embedding of the current location.
tok_emb + pos_embAdd token information and location information.
rmsnorm(x)Normalizes the input vector size.

Transformer does not process the token id itself directly. First, change the token id to vector.

token_id -> token embedding vector

However, attention alone cannot tell the order of tokens. So, you need to include location information as well.

token embedding + position embedding

[Transformer paper] (https://arxiv.org/abs/1706.03762) also explains that in structures without recurrence and convolution, positional encoding is added to input embedding to use sequence order information.


13. Making Q, K, V

When entering the Transformer block, we first prepare attention.

x_residual = xx = rmsnorm(x) q = linear(x, state_dict[f'layer{li}.attn_wq'])k = linear(x, state_dict[f'layer{li}.attn_wk'])v = linear(x, state_dict[f'layer{li}.attn_wv']) keys[li].append(k)values[li].append(v)

Q, K, and V are the core concepts of attention.

namemeaningintuition
QueryInformation that the current token wants to find“What do I want to see?”
KeyAddress or characteristics of each token“What kind of information am I?”
ValueWhat will actually be delivered“What do I have to give?”

keys[layer].append(k)andvalues[layer].append(v)are small KV caches. Because only the key/value up to the current location is accumulated, the model cannot see future tokens.

In a typical decoder-only transformer, a causal mask is used to mask future tokens.microgpt.pyprocesses tokens one by one from left to right rather than inserting the entire sequence at once. So the autoregressive condition holds without an explicit causal mask.

What you can see at your current position:[BOS, previous characters, current character] What you can't see in your current position:future characters

14. Deconstructing multi-head attention

Ifn_embd = 16,n_head = 4, the dimension of head is 4.

Full embedding 16 dimensions head 0: 0~3head 1: 4~7head 2: 8~11head 3: 12~15

Instead of performing one large attention, we divide the vector into pieces and perform attention from multiple perspectives.

for h in range(n_head):    hs = h * head_dim    q_h = q[hs:hs+head_dim]    k_h = [ki[hs:hs+head_dim] for ki in keys[li]]    v_h = [vi[hs:hs+head_dim] for vi in values[li]]

The attention score is calculated using the scaled dot-product.

attn_logits = [    sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5    for t in range(len(k_h))]attn_weights = softmax(attn_logits)

[Transformer thesis] (https://arxiv.org/abs/1706.03762) explains that the dot product of the query and key is divided by the square root of the key dimension and then softmax is applied to create a weight for the value.

Then the values ​​are weighted.

head_out = [    sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h)))    for j in range(head_dim)]x_attn.extend(head_out)

In other words, attention answers the following questions:

For the current token to predict the next token,Which of the previous tokens should I refer to and how much?

15. Attention output and residual connection

After concatenating the results of each head, output projection is performed.

x = linear(x_attn, state_dict[f'layer{li}.attn_wo'])x = x + x_residual

The first line shuffles the output of multiple heads back into the embedding dimension.

multi-head output -> output projection

The second line is the residual connection.

x = attention_output + original_input

Residual connections help information and gradients flow well in deep networks. Transformer papers also use residual connections around each sub-layer.

Althoughmicrogpt.pyhas only one layer, the structure itself follows the basic pattern of the GPT block.

RMSNorm-> Multi-head Attention-> Output Projection-> Residual Add

16. MLP block andlm_head

Attention is followed by an MLP block.

x_residual = xx = rmsnorm(x) x = linear(x, state_dict[f'layer{li}.mlp_fc1'])x = [xi.relu() for xi in x]x = linear(x, state_dict[f'layer{li}.mlp_fc2']) x = x + x_residual

If you write the structure in one line, it looks like this.

x -> RMSNorm -> Linear -> ReLU -> Linear -> Residual Add

In size, it looks like this:

16 -> 64 -> 16

If attention determines “which past token to look at,” MLP nonlinearly transforms the hidden representation of each location. The Transformer paper also uses a position-wise feed-forward network in addition to the attention sub-layer.

Finally, it passes through the output head.

logits = linear(x, state_dict['lm_head'])return logits

logitsis not yet a probability.

logits = raw score for next token candidates

For example, if it isvocab_size = 27, there are also 27 logits. If softmax is applied here, it becomes the next token probability.

probs = softmax(logits)

Ultimately, thegpt()function does the following:

Receive the current token, current position, and past KV cacheReturns logits for the next token.

17. Learning loop: turning one name into a next-token problem

The learning loop selects one document at each step and creates a token sequence prefixed withBOS.

for step in range(num_steps):    doc = docs[step % len(docs)]    tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]    n = min(block_size, len(tokens) - 1)
cordmeaning
docs[step % len(docs)]Select a document.
encode(doc)Converts characters to token id.
[BOS] +... + [BOS]Append start and end tokens.
min(block_size,...)Trim it so that it does not exceed the maximum context length.

For example,doc = "emma"would be the following sequence.

[BOS, e, m, m, a, BOS]

This sequence changes to the following problems during learning.

input tokenCorrect answer token
BOSe
em
mm
ma
aBOS

This is the basic goal of GPT learning.

Guess the next token based on the tokens you have seen so far.

18. Loss calculation

The forward pass and loss calculations have the following structure.

keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]losses = [] for pos_id in range(n):    token_id, target_id = tokens[pos_id], tokens[pos_id + 1]    logits = gpt(token_id, pos_id, keys, values)    probs = softmax(logits)    loss_t = -probs[target_id].log()    losses.append(loss_t) loss = (1 / n) * sum(losses)

The key is the negative log likelihood of the correct answer token probability.

loss = -log(P(correct_next_token))

The higher the probability of the correct answer token, the smaller the loss.

P(correct) = 0.9-log(0.9) ~= 0.105

If the probability of the correct answer token is low, the loss increases.

P(correct) = 0.01-log(0.01) ~= 4.605

In other words, the model is trained to change the weight to give a higher probability to the actual next character.


19.loss.backward(): Flow gradient to all parameters

Once loss is created, backpropagation is performed.

loss.backward()

This one line callsValue.backward()created earlier. In reality, we follow the following path backwards:

loss-> log-> softmax-> logits-> lm_head-> MLP-> attention-> embedding-> parameters

Each parameter is aValueobject. Therefore, afterloss.backward(), thegradfield of each parameter is filled.

p.grad

This value has the following meaning:

If I change this parameter slightly, how much does the loss change?

In practical terms, this perspective is also important when understanding PyTorch'sloss.backward(). This one line isn't magic, it's the process of applying the chain rule by following the computation graph backwards.


20. Direct implementation of Adam optimizer

Now we update the parameters using gradient.

learning_rate = 0.01beta1 = 0.85beta2 = 0.99eps_adam = 1e-8 m = [0.0] * len(params)v = [0.0] * len(params)

Adam paper introduces Adam as a stochastic optimization algorithm that uses adaptive estimates of the lower-order moments of the gradient.

variablemeaning
learning_rateDetermines how much to move the parameter at a time.
beta1The decay coefficient of the gradient moving average.
beta2Gradient is the decay coefficient of the squared moving average.
epsPrevents division by zero.
mThis is the first moment buffer.
vThis is the second moment buffer.

The update proceeds as follows:

1. As the step progresses, the learning rate decreases.2. Update the moving average m of the gradient.3. Update the moving average v of the gradient squared.4. Correct the bias that occurs in the initial step.5. Modify the parameter value directly.6. Initialize gradient to 0.

The core of the original update loop is this part.

lr_t = learning_rate * (1 - step / num_steps) for i, p in enumerate(params):    m[i] = beta1 * m[i] + (1 - beta1) * p.grad    v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2     m_hat = m[i] / (1 - beta1 ** (step + 1))    v_hat = v[i] / (1 - beta2 ** (step + 1))     p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam)    p.grad = 0

The last line is especially important.

p.grad = 0

If you do not initialize the gradient, the gradient of the next step will be accumulated in the previous step. In PyTorch, the role is similar to the following code.

optimizer.zero_grad()optimizer.step()

21. Inference: Starting from BOS and generating one letter at a time

Once training is complete, a new name is generated.

temperature = 0.5 for sample_idx in range(20):    keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]    token_id = BOS    sample = []

The creation loop looks like this:

for pos_id in range(block_size):    logits = gpt(token_id, pos_id, keys, values)    probs = softmax([l / temperature for l in logits])     token_id = random.choices(        range(vocab_size),        weights=[p.data for p in probs]    )[0]     if token_id == BOS:        break     sample.append(uchars[token_id])

The creation flow is as follows:

BOSto sample first letterto sample the second letter->...Quit when to BOS appears.

temperature controls the randomness of generation.

temperatureeffect
lownessHigh probability tokens are strongly preferred. The results are stable but can be monotonous.
heightEven low probability tokens have a greater chance of being selected. Results vary, but can get weird.

Here it istemperature = 0.5. Rather than generating something completely random, it gives a stronger preference to characters that the model deems plausible.


22. Revisit the entire structure at once

The overall structure ofmicrogpt.pycan be summarized as follows.

panelcode elementMeaning of GPT perspective
data preparationdocsStudy document list
tokenizeruchars,BOS,vocab_sizeConvert string to token id
autogradValueScalar-based computational graph and gradient
parameterstate_dictWeight to be learned by the model
embeddingwte,wpeConvert token and position to vector
attentionQ/K/V projectionRefer to past tokens
MLPmlp_fc1,mlp_fc2Convert hidden representation
outputlm_headGenerate the following token logits
loss-log(prob[target])Objective function to increase the probability of the correct answer token
backwardloss.backward()gradient calculation
optimizerAdam updateparameter modification
inferenceprobability samplingNext token generation

If you boil the main point down to one sentence, it's like this.

microgpt.py is an executable anatomy diagram that allows GPT's training and inference to be traced by hand without PyTorch.

The differences from the actual LLM should also be clarified.

itemmicrogpt.pyReal LLMs
tokenizercharacter unitBPE, SentencePiece, tiktoken, etc.
operation unitscalarValuetensor
training datalist of nameslarge corpus
model sizeThousands of parameter levelsParameters available from billions to trillions
learning styleProcess documents one by one sequentiallybatch, distributed training
accelerationdoesn't existGPU/TPU, fused kernel
purposeFor educational useproduction or research

23. What’s especially worth learning from this code?

It would be a shame to see this code as simply a "small GPT implementation". There are three more important learning points: Once these three things are captured, Transformer code written in PyTorch becomes much less opaque.

23.1 Autograd is a computational graph and chain rule

TheValueclass is a miniature version of PyTorch autograd.

Graph creation when forwardingApply chain rule when to backwardto parameter.grad fill

If you see this process in person, the following code will be much clearer.

loss.backward()optimizer.step()optimizer.zero_grad()

23.2 Attention is the comparison of Q and K, the weighted sum of V

The core of attention is three lines:

score = q dot k / sqrt(d)weight = softmax(score)output = sum(weight * value)

In other words, the current token does not view all past tokens equally, but gives higher weight to the necessary tokens.

23.3 GPT learning is a process of increasing the probability of the next token.

The learning objectives are not complicated.

Guess the next token with the tokens seen so far.

loss is the negative log likelihood of the correct answer token probability.

loss = -log(P(correct_next_token))

This principle remains a central idea in both small name generation models and large language models.


24. Practical checklist

If you're following the original code directly, it's a good idea to look at it in the order below.

microgpt.py reading checklist [ ] I checked the original gist reference date.[ ] I understand the flow of downloading names.txt when there is no input.txt.[ ] I understood that one docs is one name.[ ] I understand that uchars, BOS, and vocab_size act as tokenizers.[ ] Can explain the difference between Value.data and Value.grad.[ ] I understand how add and multiply store local gradients.[ ] Can explain topological sort and chain rule of backward().[ ] The roles of wte, wpe, and lm_head can be distinguished.[ ] Can explain the roles of Q, K, and V projection.[ ] I understood attention score = q dot k / sqrt(head_dim).[ ] I understand that the keys and values ​​list acts as a small KV cache.[ ] I understand that the MLP block has a 16 to 64 to 16 structure.[ ] The meaning of loss = -log(prob[target]) can be explained.[ ] Adam's m, v, and bias correction can be roughly explained.[ ] I understand what temperature does in inference.

25. Q&A

Q1. Is this code really GPT?

Structurally, it is a decoder-only Transformer-based next-token model. However, the size is very small, the tokenizer is in character units, and the training data is also a list of names.

Therefore, it is more accurate to view it as an educational miniature to understand the GPT structure rather than an actual implementation of GPT.

Q2. Why didn't you use PyTorch?

This is because the goal is algorithmic description, not performance.

With PyTorch,Tensor,Module,autograd,optimizerhide a lot of work. In practice, that is an advantage. However, for learning purposes, the internal structure may not be visible.

microgpt.pydeliberately exposes its hidden parts.

Q3. Why isBOSalso used as the end token?

Because of simplification.

In actual tokenizer,BOSandEOSare usually separated. However, since this code deals with the small problem of name generation, it uses one special token for both the start and end.

Q4. There is no causal mask, so why not look at the future token?

This code does not process the entire sequence at once. Each token is processed one by one from left to right, and only the current key/value is stored in the cache.

Therefore, at the current position, the key/value of the future token does not yet exist.

Instead of covering the future with a mask,We proceed without calculating the future at all.

Q5. If I grow this code as is, will I get an LLM?

Conceptually, it's the right direction, but it's difficult as is. A real LLM requires the following elements:

large-scale tokenizerLarge dataset pipelinebatch trainingtensor operationGPU/TPU accelerationmixed precisiondistributed trainingcheckpointevaluationserving stackSecurity and cost management

microgpt.pyis a code that shows the framework of the algorithm. The practical LLM has a huge system engineering on top of it.

Q6. What is the most important line in this code?

Personally, there are three streams.

child.grad += local_grad * node.grad

This line is the chain rule, the core of autograd.

attention_score = q dot k / sqrt(head_dim)

This expression is the core of attention.

loss = -log(P(correct_next_token))

This expression shows the next-token prediction learning goal.


26. References and uncertainty

References

confirmed facts

  • As of confirmation on 2026-05-05, the original gist contains themicrogpt.pyfile, and is displayed asLast active May 5, 2026 01:01on the GitHub Gist page.
  • The original code includes data loading, tokenizer,Valueautograd, Transformer forward, Adam learning loop, and inference sampling.
  • The original code comments explain that this model follows GPT-2, but uses RMSNorm instead of LayerNorm, no bias, and ReLU instead of GeLU.

Author's interpretation

  • This code is not an implementation to actually service GPT, but is more of an educational reference to follow the GPT algorithm by hand.
  • TheValueclass is a good starting point for understanding PyTorch autograd.
  • Attention implementation is not a tensor operation, but a Python list and scalar operation, so it is slow, but the structure is easy to read.

uncertainty

  • Since the gist may be modified, you must re-check the original revision before actual learning or reproduction.
  • Execution time may vary depending on Python version, CPU performance, and local environment.
  • The commentary in this article is based on the original gist structure confirmed on 2026-05-05.

finish

In summary,microgpt.pyis closer to a code that removes unnecessary devices to understand GPT, rather than a code that makes GPT smaller.

Creating a real LLM requires a lot more systems. The tokenizer changes, the training data grows, and batch training, GPU kernel, distributed training, checkpoint, and serving stack are added. However, most of the central flow of GPT is contained within this file.

character to tokentoken -> embeddingembedding -> attentionattention -> MLPMLP -> logitslogits -> lossloss -> gradientgradient -> Adam updateBOS -> sampling -> generated text

When first reading, it is better to look atValue.backward()before thegpt()function. The reason is simple. The greatest value of this file is not that it “implements a Transformer,” but that it shows how forward computation becomes a graph, and how that graph returns to a gradient.

Then, if you follow the sequence oflinear(),softmax(), attention score calculation, and loss calculation, the entire structure becomes clear. If you understand this code, Transformer code written in PyTorch will no longer look like two magic lines:loss.backward()andoptimizer.step().

댓글

GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions를 통해 운영됩니다.

TOP