nanoGPT is Andrej Karpathy's minimal, hackable reference implementation of GPT
for training and finetuning small-to-medium language models. The entire model
fits in a single ~330-line model.py, the training loop is one ~330-line
train.py, and configuration is a "poor man's configurator" that runs Python
config files and CLI overrides through exec() against module globals. The
repo is not a Python package — there is no pip install nanogpt. The intended
workflow is git clone, install the PyTorch ecosystem, then run scripts from
the repo root.
This guide covers everything a user needs to train, sample, finetune, and benchmark with nanoGPT. The Shakespeare-character demo trains a fully working baby GPT in under ten minutes on a single GPU and remains the fastest way to verify your installation.
1. Quick Start
The fastest path from an empty directory to a trained, sampling baby GPT is three commands plus a clone. Run all of them from the repository root.
git clone https://github.com/karpathy/nanoGPT.git
cd nanoGPT
pip install torch numpy transformers datasets tiktoken wandb tqdm
python data/shakespeare_char/prepare.py
python train.py config/train_shakespeare_char.py
python sample.py --out_dir=out-shakespeare-char
Tip
If you are on a MacBook (no CUDA), append
--device=cpu --compile=False --eval_iters=20 --log_interval=1 --block_size=64 --batch_size=12 --n_layer=4 --n_head=4 --n_embd=128 --max_iters=2000 --lr_decay_iters=2000 --dropout=0.0to thetrain.pyinvocation. The character-level Shakespeare recipe is intentionally tiny so it runs end-to-end on a laptop CPU in a reasonable amount of time.
What just happened
| Step | Effect |
|---|---|
git clone |
Pulls the source tree (5 Python files at root + config/ + data/). |
pip install |
Installs PyTorch, NumPy, HuggingFace Transformers, HuggingFace Datasets, tiktoken, wandb, tqdm. |
prepare.py |
Downloads tinyshakespeare/input.txt, builds a character-level vocabulary (65 unique chars), writes train.bin, val.bin, and meta.pkl. |
train.py |
Trains a 6-layer / 6-head / 384-embed model for 5,000 iterations (~10 minutes on a single A100, longer on consumer cards). |
sample.py |
Loads out-shakespeare-char/ckpt.pt and prints 10 samples of 500 tokens each. |
Key features
- Single-file model in
model.py(LayerNorm,CausalSelfAttention,MLP,Block,GPTConfig,GPT). Around 330 lines, fully readable in one sitting. - Single-file training loop in
train.pywith cosine learning-rate decay, gradient accumulation, mixed-precision autocast, optionaltorch.compile, and DDP support out of the box. - Loads HuggingFace GPT-2 weights (
gpt2,gpt2-medium,gpt2-large,gpt2-xl) for finetuning or zero-shot evaluation viaGPT.from_pretrained(). - Flash Attention path activates automatically on PyTorch 2.0+
(
scaled_dot_product_attention). - Optional Weights & Biases logging and
bench.pymicro-benchmarking script withtorch.profilerintegration. - Five reference configs in
config/: train GPT-2 124M, finetune Shakespeare, evaluate the four GPT-2 sizes, and the Shakespeare-char baby model.
2. Installation
nanoGPT is a clone-and-run repository. There is no PyPI package, no Docker image in the repo, and no installation script — you clone the source, install the PyTorch ecosystem, and run scripts directly.
System requirements
| Resource | Recommended | Minimum |
|---|---|---|
| Python | 3.10+ | 3.8 |
| PyTorch | 2.0+ (Flash Attention path enabled) | 1.13 (manual attention fallback) |
| GPU | NVIDIA A100/A6000/4090 with bfloat16 | Single consumer GPU, or CPU/MPS for the baby model |
| RAM | 16 GB | 8 GB |
| Disk | 60 GB free for OpenWebText pretraining | 200 MB for Shakespeare-char |
Important
The full GPT-2 124M reproduction requires roughly 1 node × 8 × A100 40GB for ~5 days (per the comment at the top of
config/train_gpt2.py). The Shakespeare-char demo runs comfortably on a single consumer GPU — or a laptop CPU with the size overrides shown in Quick Start.
Step 1: Clone the repository
git clone https://github.com/karpathy/nanoGPT.git
cd nanoGPT
Step 2: Install dependencies
There is no requirements.txt shipped with this repo. The dependencies are
the ones imported by the scripts:
pip install torch numpy transformers datasets tiktoken wandb tqdm
| Package | Used by | Purpose |
|---|---|---|
torch |
every script | The model, optimizer, autocast, DDP, torch.compile. |
numpy |
train.py, bench.py, data/*/prepare.py |
np.memmap over train.bin/val.bin for the data loader. |
transformers |
model.py:GPT.from_pretrained |
Loads GPT-2 124M / medium / large / xl weights from HuggingFace. |
datasets |
data/openwebtext/prepare.py |
HuggingFace load_dataset("openwebtext"). |
tiktoken |
sample.py, data/shakespeare/prepare.py, data/openwebtext/prepare.py |
GPT-2 BPE encoder/decoder. |
wandb |
train.py (only if wandb_log=True) |
Optional experiment tracking. |
tqdm |
data/openwebtext/prepare.py |
Progress bar for the long tokenization step. |
requests |
data/shakespeare*/prepare.py |
Downloads the tinyshakespeare corpus. Bundled with most Python distributions. |
Note
If you only plan to run the Shakespeare demos and never touch OpenWebText, you can omit
datasets. Conversely, if you never plan to load HuggingFace GPT-2 weights, you can omittransformers. The scripts only import what they need.
Step 3: Verify the install
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
2.5.1+cu121 True
If torch.cuda.is_available() returns False and you have an NVIDIA GPU,
reinstall PyTorch with the CUDA wheel that matches your driver — see
pytorch.org.
Step 4: Prepare a dataset
Pick one of the three bundled datasets:
| Dataset | Prepare command | Output | Size |
|---|---|---|---|
| Shakespeare-char | python data/shakespeare_char/prepare.py |
65-char vocab, train.bin (~1M tokens), val.bin, meta.pkl |
~2 MB |
| Shakespeare (BPE) | python data/shakespeare/prepare.py |
GPT-2 BPE, train.bin (301,966 tokens), val.bin (36,059 tokens) |
~700 KB |
| OpenWebText | python data/openwebtext/prepare.py |
GPT-2 BPE, train.bin (~17 GB, ~9B tokens), val.bin (~8.5 MB, ~4M tokens) |
~54 GB cache + ~17 GB output |
Warning
data/openwebtext/prepare.pydownloads roughly 54 GB into the HuggingFace cache and writes another ~17 GB oftrain.bin. It is single-shot but takes hours and a fast disk. Start with Shakespeare-char to confirm everything works.
Step 5: Run the Shakespeare-char demo
python train.py config/train_shakespeare_char.py
You should see output like:
tokens per iteration will be: 16,384
Initializing a new model from scratch
defaulting to vocab_size of GPT-2 to 50304 (50257 rounded up for efficiency)
number of parameters: 10.65M
num decayed parameter tensors: 26, with 10,727,424 parameters
num non-decayed parameter tensors: 13, with 4,992 parameters
using fused AdamW: True
compiling the model... (takes a ~minute)
step 0: train loss 4.2876, val loss 4.2823
iter 0: loss 4.2841, time 6829.40ms, mfu -100.00%
iter 10: loss 3.2447, time 23.65ms, mfu 5.46%
...
step 5000: train loss 0.6178, val loss 1.4974
When training finishes (~5,000 iters), a checkpoint will live at
out-shakespeare-char/ckpt.pt.
3. Configuration
nanoGPT uses a deliberately bare-bones configuration mechanism nicknamed the
"poor man's configurator." It lives in configurator.py and works like this:
train.py,sample.py, andbench.pydeclare default values as module-level globals.- They call
exec(open('configurator.py').read()). configurator.pywalkssys.argv[1:]. If an arg has no=, it is treated as a Python config file andexec()'d into the calling module's globals. If an arg starts with--key=value, it overrides the matching global by parsingvaluewithast.literal_eval.- Any unknown key raises
ValueError: Unknown config key: {key}.
The override pattern
# config file only
python train.py config/train_shakespeare_char.py
# config file + CLI overrides (overrides win)
python train.py config/train_shakespeare_char.py --batch_size=32 --device=cpu
# CLI overrides only, all defaults from train.py
python train.py --batch_size=32 --compile=False
Important
Order matters. The config file is
exec()'d first, then--key=valueoverrides are applied after. CLI flags always win over config-file values, which always win over thetrain.pydefaults.
Config file shape
A config file is just plain Python. It assigns values to the same names that
train.py/sample.py/bench.py declare as defaults. Example
(config/train_shakespeare_char.py):
out_dir = 'out-shakespeare-char'
eval_interval = 250
eval_iters = 200
log_interval = 10
always_save_checkpoint = False
wandb_log = False
wandb_project = 'shakespeare-char'
wandb_run_name = 'mini-gpt'
dataset = 'shakespeare_char'
gradient_accumulation_steps = 1
batch_size = 64
block_size = 256
n_layer = 6
n_head = 6
n_embd = 384
dropout = 0.2
learning_rate = 1e-3
max_iters = 5000
lr_decay_iters = 5000
min_lr = 1e-4
beta2 = 0.99
warmup_iters = 100
Most-used training settings
The full reference is in the Admin & Developer Guide. The settings below are the ones users tune most often.
| Setting | Default in train.py |
Meaning |
|---|---|---|
out_dir |
'out' |
Checkpoint and log directory. Created if missing. |
dataset |
'openwebtext' |
Folder name under data/ containing train.bin/val.bin. |
init_from |
'scratch' |
One of 'scratch', 'resume', 'gpt2', 'gpt2-medium', 'gpt2-large', 'gpt2-xl'. |
batch_size |
12 |
Micro-batch size. With gradient_accumulation_steps > 1 this is the per-step micro-batch. |
block_size |
1024 |
Context window in tokens. |
gradient_accumulation_steps |
40 (5 × 8) |
Number of micro-steps per optimizer step. |
n_layer / n_head / n_embd |
12 / 12 / 768 |
Model size (default GPT-2 124M). |
dropout |
0.0 |
0.0 for pretraining, ~0.1+ for finetuning. |
bias |
False |
Whether nn.Linear and LayerNorm use bias terms. |
learning_rate |
6e-4 |
Peak LR after warmup. |
min_lr |
6e-5 |
Floor LR after cosine decay. Karpathy recommends ≈learning_rate / 10. |
warmup_iters |
2000 |
Linear warmup iterations. |
lr_decay_iters |
600000 |
Iteration at which cosine decay reaches min_lr. Per Chinchilla, set ≈ max_iters. |
max_iters |
600000 |
Total training iterations. |
weight_decay |
1e-1 |
AdamW weight decay (only on 2D tensors). |
beta1 / beta2 |
0.9 / 0.95 |
AdamW betas. |
grad_clip |
1.0 |
Gradient norm clipping value. 0.0 disables. |
decay_lr |
True |
Toggle the cosine LR scheduler. |
eval_interval |
2000 |
Iters between train/val loss evaluations + checkpoint write. |
eval_iters |
200 |
Number of batches averaged when computing eval loss. |
eval_only |
False |
If True, run eval once and exit. |
always_save_checkpoint |
True |
If False, only save when val loss improves. |
device |
'cuda' |
'cuda', 'cpu', 'cuda:0', 'mps'. |
dtype |
'bfloat16' if supported else 'float16' |
Autocast dtype. 'float16' activates GradScaler automatically. |
compile |
True |
torch.compile(model) for ~10-30% speedup on PyTorch 2.0+. |
wandb_log / wandb_project / wandb_run_name |
False / 'owt' / 'gpt2' |
Optional Weights & Biases logging. |
backend |
'nccl' |
DDP backend ('nccl' for GPU, 'gloo' for CPU/cross-platform). |
Most-used sampling settings
| Setting | Default in sample.py |
Meaning |
|---|---|---|
init_from |
'resume' |
'resume' loads out_dir/ckpt.pt; 'gpt2*' loads HuggingFace weights. |
out_dir |
'out' |
Checkpoint directory (only used with init_from='resume'). |
start |
'\n' |
Prompt. Use "FILE:prompt.txt" to read from a file. |
num_samples |
10 |
Number of samples to generate. |
max_new_tokens |
500 |
Tokens per sample. |
temperature |
0.8 |
<1.0 more conservative, >1.0 more random. |
top_k |
200 |
Truncate to top-k logits before sampling. |
seed |
1337 |
RNG seed. |
device / dtype / compile |
'cuda' / autodetect / False |
Same semantics as train.py. |
Tip
The sampling defaults are deliberately chatty (
num_samples=10,max_new_tokens=500). Knock those down with--num_samples=1 --max_new_tokens=100for quick sanity checks.
4. Core Usage
The three scripts you will run day-to-day are train.py, sample.py, and
bench.py. They share the configurator, so everything below works with both
config-file syntax and --key=value overrides.
Training (train.py)
train.py is a single while-loop that:
- Resolves config (defaults → config file → CLI overrides).
- Detects DDP from the
RANKenvironment variable. If unset, runs single process. - Builds an
np.memmapdata loader overdata/{dataset}/train.binandval.bin. - Instantiates the model from one of three init paths:
'scratch','resume', or'gpt2*'. - Configures AdamW via
model.configure_optimizers(2D tensors get weight decay, 1D tensors don't). - Optionally
torch.compiles the model. - Wraps in DDP if
RANK >= 0. - Runs the training loop with cosine LR scheduling, gradient accumulation,
bfloat16/float16autocast, and optionalGradScalerforfloat16. - Every
eval_intervaliterations, evaluatestrainandvalloss overeval_itersbatches and writesckpt.ptifvalimproved (or always, ifalways_save_checkpoint=True).
Single-GPU training
python train.py config/train_shakespeare_char.py
python train.py --batch_size=32 --compile=False
Distributed training (DDP) — single node
torchrun --standalone --nproc_per_node=4 train.py config/train_gpt2.py
torchrun sets RANK, LOCAL_RANK, and WORLD_SIZE for each process.
train.py reads them, assigns each rank to cuda:{LOCAL_RANK}, picks the
rank-0 process as master_process (handles logging, eval, and checkpointing),
and divides gradient_accumulation_steps by WORLD_SIZE.
Distributed training — multi-node
# master node (123.456.123.456)
torchrun --nproc_per_node=8 --nnodes=2 --node_rank=0 \
--master_addr=123.456.123.456 --master_port=1234 train.py
# worker node
torchrun --nproc_per_node=8 --nnodes=2 --node_rank=1 \
--master_addr=123.456.123.456 --master_port=1234 train.py
Note
If your cluster lacks Infiniband, prepend
NCCL_IB_DISABLE=1to thetorchruncommand on every node. This is documented in the docstring at the top oftrain.py.
Resuming an interrupted run
python train.py config/train_gpt2.py --init_from=resume
init_from='resume' loads out_dir/ckpt.pt, restores iter_num and
best_val_loss, and forces n_layer, n_head, n_embd, block_size,
bias, vocab_size to match the checkpoint (other args like dropout can
still be tuned).
Important
Keep
out_dirstable when resuming. The script reads{out_dir}/ckpt.ptdirectly. If you changeout_dir, copy the checkpoint over first.
Sampling (sample.py)
# from a checkpoint you trained
python sample.py --out_dir=out-shakespeare-char
# from a HuggingFace GPT-2 checkpoint
python sample.py --init_from=gpt2-xl
# with a prompt
python sample.py --out_dir=out-shakespeare-char --start="ROMEO:"
# from a prompt file
python sample.py --out_dir=out --start="FILE:prompt.txt" --num_samples=3
If the checkpoint's training config used a dataset with a meta.pkl
(character-level), sample.py loads data/{dataset}/meta.pkl and uses its
stoi/itos mapping. Otherwise it falls back to the GPT-2 BPE encoder via
tiktoken.get_encoding("gpt2").
Benchmarking (bench.py)
bench.py is a stripped-down train.py that runs a fixed number of forward,
backward, and optimizer steps and reports time-per-iteration plus model FLOPs
utilization (MFU).
python bench.py
python bench.py --compile=False --real_data=False --profile=True
bench.py setting |
Default | Meaning |
|---|---|---|
batch_size |
12 |
Same as in train.py. |
block_size |
1024 |
Same as in train.py. |
bias |
False |
Same as in train.py. |
real_data |
True |
If False, generate random int tensors instead of memmapping train.bin. |
seed |
1337 |
RNG seed. |
device |
'cuda' |
Same as in train.py. |
dtype |
autodetect | Same as in train.py. |
compile |
True |
Same as in train.py. |
profile |
False |
If True, run torch.profiler and emit traces to ./bench_log for TensorBoard. |
Mixed precision — bfloat16 vs float16
train.py autodetects: if the GPU supports bfloat16
(torch.cuda.is_bf16_supported()), it picks 'bfloat16'; otherwise
'float16'. The difference matters:
- bfloat16: same exponent range as
float32, noGradScalerneeded. The cleaner default on Ampere (A100, A6000), Hopper (H100), and Ada (RTX 4090). - float16: narrower exponent range, requires
torch.cuda.amp.GradScaler, whichtrain.pyenables automatically whendtype == 'float16'.
Warning
If you force
--dtype=bfloat16on a card that doesn't support it (anything pre-Ampere), training will silently fall back to slow paths or NaN out. Either let the autodetect run or explicitly set--dtype=float16.
Checkpoint contents
A checkpoint is a Python dict pickled by torch.save:
{
'model': state_dict, # raw_model.state_dict()
'optimizer': state_dict, # optimizer.state_dict()
'model_args': dict, # n_layer, n_head, n_embd, block_size, bias, vocab_size, dropout
'iter_num': int, # last completed iter
'best_val_loss': float, # for early stopping decisions
'config': dict, # all of train.py's config_keys snapshot
}
Checkpoints land at {out_dir}/ckpt.pt. There is one checkpoint per
out_dir — the file is overwritten each save.
5. Common Tasks
Task 1: Train the Shakespeare-char baby GPT in under 10 minutes
python data/shakespeare_char/prepare.py
python train.py config/train_shakespeare_char.py
python sample.py --out_dir=out-shakespeare-char --start="JULIET:"
This trains a 6-layer / 6-head / 384-embed model (~10.65M params) for 5,000
iterations at batch_size=64, block_size=256, character-level vocab
(65 unique chars). Final val loss lands around 1.5, which produces
recognizable mock-Shakespeare.
Task 2: Finetune a public GPT-2 checkpoint on Shakespeare
python data/shakespeare/prepare.py
python train.py config/finetune_shakespeare.py
python sample.py --out_dir=out-shakespeare --start="To be or not to be,"
config/finetune_shakespeare.py sets init_from='gpt2-xl',
learning_rate=3e-5, decay_lr=False, max_iters=20,
gradient_accumulation_steps=32, always_save_checkpoint=False. It only
saves when validation improves and finetunes at constant LR. Swap
init_from for 'gpt2', 'gpt2-medium', or 'gpt2-large' if gpt2-xl is
too big for your GPU.
Tip
For finetuning, bump
dropoutfrom0.0to0.1or so. The defaulttrain.pyconfig sets it to0.0because that's optimal for pretraining, but a small amount of regularization helps when training on tiny corpora.
Task 3: Sample from a HuggingFace GPT-2 checkpoint with no training
python sample.py \
--init_from=gpt2-xl \
--start="What is the answer to life, the universe, and everything?" \
--num_samples=5 --max_new_tokens=100
sample.py notices init_from.startswith('gpt2'), calls
GPT.from_pretrained('gpt2-xl', dict(dropout=0.0)), and that helper pulls
weights from HuggingFace via transformers.GPT2LMHeadModel.from_pretrained,
then maps them onto nanoGPT's vanilla nn.Linear layout (transposing the
GPT-2 Conv1D weights).
Task 4: Pretrain GPT-2 124M on OpenWebText across 8 GPUs
python data/openwebtext/prepare.py
torchrun --standalone --nproc_per_node=8 train.py config/train_gpt2.py
config/train_gpt2.py is the reference script for "GPT-2 124M down to a very
nice loss of ~2.85 on 1 node of 8X A100 40GB" (≈5 days). It sets
batch_size=12, block_size=1024, gradient_accumulation_steps=40
(5 × 8 GPUs), max_iters=600000, lr_decay_iters=600000,
weight_decay=1e-1, with wandb logging on under project 'owt'.
Task 5: Log to Weights & Biases
pip install wandb
wandb login
python train.py config/train_gpt2.py --wandb_log=True \
--wandb_project=my-experiments --wandb_run_name=run-001
When wandb_log=True and the process is master_process, train.py calls
wandb.init and logs iter, train/loss, val/loss, lr, and mfu at
each eval_interval.
Caution
Never commit your wandb API key to source control.
wandb loginwrites it to~/.netrc. If you must run on a shared box, usewandb login --reloginin your shell, not in a config file.
Task 6: Scale the model to a custom dataset
mkdir -p data/mydata
# write your own data/mydata/prepare.py that produces train.bin / val.bin
python data/mydata/prepare.py
python train.py \
--dataset=mydata \
--out_dir=out-mydata \
--n_layer=8 --n_head=8 --n_embd=512 \
--block_size=512 \
--batch_size=32 --gradient_accumulation_steps=4 \
--learning_rate=3e-4 --min_lr=3e-5 \
--max_iters=20000 --lr_decay_iters=20000 \
--warmup_iters=200 \
--eval_interval=500 --eval_iters=100
train.py reads data/{dataset}/train.bin and val.bin directly. If you
also drop a meta.pkl with vocab_size, the training run picks it up
automatically; otherwise nanoGPT defaults to vocab_size=50304 (the GPT-2
vocabulary padded up from 50,257 to the nearest multiple of 64 for kernel
efficiency).
Task 7: Prepare a custom dataset
The Shakespeare-char prepare.py is the cleanest template. The contract
train.py expects is two NumPy uint16 files:
import numpy as np
# encode your text however you want - char-level, BPE, sentencepiece, etc.
train_ids = ... # list[int]
val_ids = ... # list[int]
train_ids = np.array(train_ids, dtype=np.uint16)
val_ids = np.array(val_ids, dtype=np.uint16)
train_ids.tofile('data/mydata/train.bin')
val_ids.tofile('data/mydata/val.bin')
# optional but recommended for char-level vocabs
import pickle
with open('data/mydata/meta.pkl', 'wb') as f:
pickle.dump({'vocab_size': vocab_size, 'stoi': stoi, 'itos': itos}, f)
Note
np.uint16caps your vocab at 65,536. The GPT-2 BPE vocab (50,257) and common BPE sizes fit comfortably. If you need a vocab >65K, switch tonp.uint32and update thedtype=arguments intrain.py'sget_batchandbench.py'sget_batchto match.
Task 8: Recover or resume an interrupted run
If your training crashed or you Ctrl-C'd it:
python train.py config/train_gpt2.py --init_from=resume
init_from='resume' reads {out_dir}/ckpt.pt, restores iter_num and
best_val_loss, and continues. The optimizer state is also restored, so
the LR schedule picks up exactly where it left off.
Warning
If you change
n_layer,n_head,n_embd,block_size,bias, orvocab_sizebetween runs,--init_from=resumewill overwrite your CLI overrides with the checkpoint's values to keep the architecture compatible. Other knobs likedropoutorlearning_ratecan still be tuned freely.
6. Troubleshooting
Error: RuntimeError: Dataset scripts are no longer supported
Cause: As of recent HuggingFace datasets releases, the openwebtext
dataset stopped publishing as a scripted dataset. This is tracked upstream
in GitHub issue #633.
Solution:
- Pin an older
datasetsversion:pip install "datasets<2.20". - Or use a community mirror that still ships the parquet shards directly.
- Or replace
data/openwebtext/prepare.pywith a hand-rolled tokenizer over any large web-text corpus you can supply yourself — the only contracttrain.pycares about istrain.bin/val.binasuint16token files.
Prevention: This is a moving target on the HuggingFace side. If you must
reproduce GPT-2 124M end-to-end, lock your datasets version in a venv.
Error: ValueError: Unknown config key: {key}
Cause: You passed --{key}=value on the command line (or set it in a
config file) but {key} is not a global declared in train.py/sample.py/
bench.py. The configurator deliberately rejects typos so a misspelled flag
doesn't silently no-op.
Solution: Double-check the spelling against the tables in Section 3 of
this guide. Common gotchas: lr (the right name is learning_rate),
epochs (nanoGPT uses max_iters, not epochs), dropout_rate (the right
name is dropout).
Prevention: When in doubt, scan the # default config values block at
the top of train.py (or sample.py / bench.py) — the configurator only
recognizes names declared there.
Error: WARNING: using slow attention. Flash Attention requires PyTorch >= 2.0
Cause: model.py:CausalSelfAttention.__init__ checks
hasattr(torch.nn.functional, 'scaled_dot_product_attention'). On
PyTorch < 2.0 that attribute is missing, so the model falls back to a manual
QK^T softmax with a registered triangular causal mask buffer.
Solution: Upgrade to PyTorch 2.0 or newer:
pip install --upgrade torch
Prevention: PyTorch 2.0 has been stable for a while; pin
torch>=2.0 in any requirements file you maintain downstream.
Error: Training NaNs out within a few iterations
Cause: Common in float16 autocast when gradients overflow before
GradScaler adapts; or when the learning rate is too high; or when
gradient_accumulation_steps is set so high that loss scaling drifts. Issue
#524 (Pretraining Divergence) is the long-running upstream thread for
discussion of nanoGPT NaN modes.
Solution:
- Switch to
--dtype=bfloat16if your GPU supports it (Ampere+). - Reduce
learning_rate(try halving it). - Lower
gradient_accumulation_stepsuntil the issue stops, then bisect. - Verify
grad_clip=1.0is in effect (the default).
Prevention: On Ampere/Hopper/Ada GPUs, prefer bfloat16. The Karpathy
default already does this when bfloat16 is supported.
Error: CUDA out of memory during training
Cause: Your model + activations + optimizer state + gradients exceed GPU memory.
Solution:
- Lower
batch_sizeand raisegradient_accumulation_stepsproportionally to keep effective batch size constant. - Lower
block_size(context length is quadratic in attention memory). - Drop
compile=True—torch.compileincreases peak memory during compilation. - Switch to a smaller model: lower
n_layer,n_embd, or both.
Prevention: Use bench.py --profile=True on your target hardware before
launching the long run — the resulting traces under ./bench_log show peak
GPU allocation per step.
Error: Sampling outputs are gibberish even though training loss is low
Cause 1: Mismatched encoder. If you trained with character-level
encoding (shakespeare_char / a custom meta.pkl) but sample.py defaulted
to the GPT-2 BPE encoder, every "char" gets re-encoded as several BPE
tokens of unrelated semantics.
Cause 2: You passed --init_from=gpt2* to sample.py but pointed
--out_dir at a checkpoint trained from scratch. init_from='gpt2*'
ignores out_dir.
Solution:
- For Cause 1: confirm
data/{dataset}/meta.pklexists and thatsample.py'sLoading meta from ...print appears at startup. - For Cause 2: pick exactly one of
--init_from=resume --out_dir=...or--init_from=gpt2-xl. They're mutually exclusive.
Prevention: Always sanity-check the first two lines of sample.py
output — they tell you exactly which encoder and which weights are being
used.
Error: connection refused / DDP processes hang on multi-node setup
Cause 1: master_addr unreachable from worker nodes (firewall, wrong
IP).
Cause 2: Cluster lacks Infiniband and NCCL is trying to use it.
Solution:
- Verify worker → master TCP reachability on
master_port. - Prepend
NCCL_IB_DISABLE=1to everytorchruninvocation:NCCL_IB_DISABLE=1 torchrun --nproc_per_node=8 ... - For deeper diagnostics, set
NCCL_DEBUG=INFO.
Prevention: Test single-node DDP first
(torchrun --standalone --nproc_per_node=2 train.py) before scaling out.
Diagnostic decision tree
- Did the install verify?
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"— if not, fix PyTorch first. - Does
python data/shakespeare_char/prepare.pyproducetrain.bin? If not, it's a network or write-permission issue. - Does
python train.py config/train_shakespeare_char.pymake it paststep 0? If not, see the OOM and NaN sections above. - Does
python sample.py --out_dir=out-shakespeare-charproduce readable output? If not, see the gibberish-sampling section.
If all four pass, your installation is healthy and any failures from there are configuration- or data-specific.
7. FAQ
Q: Is nanoGPT a Python package I can pip install?
No. nanoGPT is a clone-and-run repository. There is no setup.py, no
pyproject.toml, no PyPI release. You git clone, pip install the
PyTorch ecosystem manually, and run python train.py from the repo root.
Q: What is the smallest GPU I can train GPT-2 124M on?
config/train_gpt2.py was tuned for 8 × A100 40GB. To train 124M on a
single GPU you'll need to keep the effective batch size by raising
gradient_accumulation_steps and lowering batch_size. Even with that,
expect dozens of days of compute. The Shakespeare-char demo is the realistic
single-GPU and single-CPU target for hobbyists.
Q: Why vocab_size=50304 instead of the actual GPT-2 size of 50257?
train.py rounds 50257 up to the nearest multiple of 64 (= 50304) because
"50304 % 64 == 0" lets the GPU run faster matmul kernels. The model wastes
47 unused embedding rows but in exchange every Linear layer hits an
efficient tiling. The from_pretrained path forces vocab_size=50257
because the OpenAI GPT-2 checkpoints were trained with that size — but the
default scratch path bumps it back up to 50304.
Q: How do I disable torch.compile?
Pass --compile=False on the CLI, or set compile = False in your config
file. torch.compile requires PyTorch 2.0 and adds a ~1-minute warmup. On
small models or quick experiments, it's often slower overall.
Q: Why does the validation loss differ slightly between runs even with fixed seed?
torch.manual_seed(1337 + seed_offset) is called once at startup, but
non-determinism creeps in via cuDNN, async CUDA streams, and torch.compile
fusions. For bit-exact reproducibility, also set
torch.use_deterministic_algorithms(True) — but expect a slowdown.
Q: Where do I look for the model architecture?
Everything lives in model.py (~330 lines):
LayerNorm— line 18CausalSelfAttention— line 29MLP— line 78Block— line 94GPTConfig(dataclass) — line 109GPT— line 118 (withfrom_pretrained,configure_optimizers,estimate_mfu,crop_block_size,generate)
Q: Can I train on Apple Silicon?
Yes for the small demos. Set --device=mps --dtype=float16 --compile=False
on the train.py and sample.py commands. torch.compile on MPS is
unstable, and bfloat16 on MPS isn't supported on all releases.
Q: How do I make sampling deterministic?
sample.py already calls torch.manual_seed(seed) and
torch.cuda.manual_seed(seed) with seed=1337 by default. Override with
--seed=42 if you want a fresh stream. For top-p (nucleus) sampling, pass
--top_k=None and adjust --temperature instead — there is no native
top-p flag.
Q: Where are the GPT-2 weights downloaded to?
GPT.from_pretrained calls transformers.GPT2LMHeadModel.from_pretrained
which uses HuggingFace's standard cache (~/.cache/huggingface/hub/ by
default, overridable via HF_HOME).
Q: Is there an HTTP API or a CLI binary?
No. nanoGPT is intentionally a script-only repo. There are no exposed
endpoints, no argparse CLI, no installable entry points. Every interaction
is python train.py … or python sample.py … from the repo root.
Q: How do I cite nanoGPT?
Per the LICENSE in the repo (MIT), you don't have to. If you want to, referencing the GitHub URL and commit hash is the standard practice for research code without a published paper.