Live Demo:https://nanogpt.streamlit.app
A CPU-friendly character-level GPT from scratch in PyTorch featuring modern architecture (RoPE, RMSNorm, GQA) and a modular RAG pipeline with Chroma DB. No Hugging Face, no magic — every line is readable and educational.
Note: This model is currently trained exclusively on Shakespearean text. It generates text in the style and vocabulary of Shakespeare and is not a general-purpose instruction-following model.
~250 lines of model code | ~200 lines of training code
python -m venv .venv
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
.venv/Scripts/activate
pip install -r requirements.txt
python data.py --download shakespeare
# → downloads input.txt
Or use your own text file — just point --data at it.
# CPU-friendly tiny model (~3 min on CPU)
python train.py --preset tiny
# Default model (~10M params, needs GPU or ~30 min CPU)
python train.py
# Your own corpus
python train.py --data my_notes.txt --preset tiny
Training prints loss every 100 steps and generates text samples every 500 steps:
step 500 | train loss 1.8234 | val loss 1.9102 | lr 3.00e-04 | elapsed 42s
──── sample @ step 500 ────
GLOUCESTER:
What, art thou so brave? then let us hear
The king himself hath spoke it; and for my part...
# Single prompt
python generate.py \
--checkpoint checkpoints/ckpt_02000.pt \
--prompt "To be or"
# Interactive REPL
python generate.py \
--checkpoint checkpoints/ckpt_02000.pt \
--interactive
Edit config.py or pass flags. Key knobs:
| Parameter | Default | Effect |
|---|---|---|
block_size |
256 | Context window. Longer = more memory |
n_embd |
384 | Embedding size. Larger = smarter, slower |
n_head |
6 | Attention heads. Must divide n_embd |
n_layer |
6 | Transformer depth |
dropout |
0.2 | Regularisation. 0 for tiny datasets |
batch_size |
64 | Reduce if OOM |
learning_rate |
3e-4 | AdamW LR |
temperature |
0.8 | Generation: higher = more random |
top_k |
40 | 0 = pure sampling, 40 = focused |
# In config.py
TINY_CONFIG # n_embd=128, n_layer=4 — trains in minutes on CPU
SMALL_CONFIG # n_embd=384, n_layer=6 — ~10M params, needs GPUInput token IDs → Token Embedding
+ Positional Embedding
↓
[TransformerBlock × n_layer]
┌─────────────────────────────┐
│ LayerNorm │
│ MultiHeadSelfAttention ←── │── causal mask prevents
│ + residual │ seeing future tokens
│ LayerNorm │
│ FeedForward (MLP) ←──│── per-token processing
│ + residual │
└─────────────────────────────┘
↓
LayerNorm → Linear head → logits (vocab_size)
↓
softmax → P(next character)
Key concepts implemented:
- Causal (masked) self-attention — tokens can only attend to past positions
- Multi-head attention — multiple attention patterns in parallel
- Positional embeddings — learned position encodings
- Residual connections — skip connections around each sub-layer
- Pre-LayerNorm — normalise before sub-layers (more stable than original)
- Weight tying — token embedding and LM head share weights
- Cosine LR schedule — warmup + cosine decay
- Gradient clipping — prevents exploding gradients
- Top-k sampling — controlled randomness at generation time
After you've trained this:
- Swap embeddings — try sinusoidal positional encodings instead of learned
- Flash Attention —
torch.nn.functional.scaled_dot_product_attention(one line swap, much faster) - BPE tokenization — replace
CharTokenizerwithtiktokenfor subword tokens - Fine-tuning — load a pretrained
gpt2via Hugging Face, freeze some layers - RAG — feed retrieved document chunks into the prompt instead of baking knowledge into weights
python train.py --resume checkpoints/ckpt_02000.pt
| Steps | Train Loss | Val Loss | Quality |
|---|---|---|---|
| 0 | ~4.2 | ~4.2 | Random characters |
| 500 | ~2.0 | ~2.1 | Recognisable words |
| 2000 | ~1.5 | ~1.6 | Shakespearean structure |
| 5000 | ~1.2 | ~1.4 | Convincing prose |
A fully modular RAG pipeline that allows you to perform semantic search and context injection over your corpus using ChromaDB and sentence-transformers.
pip install -r rag/requirements.txt
python rag/demo.py
------------------------- RAG Demo -------------------------
Corpus : D:\Code\Repo\SLM\nanogpt\input.txt
Index : D:\Code\Repo\SLM\nanogpt\rag_index
[1/4] Building ChromaDB index from corpus ...
This embeds ~4000 chunks — takes ~2 min on CPU, once only.
Loading embedding model: sentence-transformers/all-MiniLM-L6-v2
Embedding dim : 384
Embedding 4648 chunks from 'input.txt' ...
Added 4648 chunks | Total: 4648
VectorStore saved -> D:\Code\Repo\SLM\nanogpt\rag_index/ (4648 vectors, dim=384)
Index built in 64.2s and saved to 'D:\Code\Repo\SLM\nanogpt\rag_index/'
Index ready: 4648 chunks indexed
-------------- Retrieval Demo (no LLM needed) --------------
Query: 'What does Hamlet say about death and dying?'
[1] score=0.5423 ##########
kill your husband.
LADY ANNE:
Why, then he is alive.
GLOUCESTER:
Nay, he is dead; and slain by Edward's hand.
LADY ANNE:
In thy foul throat thou li...
Query: 'A betrayal and murder of a king'
[1] score=0.6159 ############
die your king,
As ours by murder, to make him a king!
Edward thy son, which now is Prince of Wales,
For Edward my son, which was Prince of Wales,
Die...
Query: 'Love and romantic feelings'
[1] score=0.4751 #########
ng first create!
O heavy lightness! serious vanity!
Mis-shapen chaos of well-seeming forms!
Feather of lead, bright smoke, cold fire,
sick health!
Sti...
To run with full LLM-augmented generation:
- Download Ollama
- Run
ollama pull llama3.2:1b - Start the Ollama server:
ollama serve - Run
python rag/demo.pyagain.
You can easily deploy this model and the RAG pipeline as a web application on Streamlit Cloud for free.
- Ensure your trained checkpoint (e.g.,
ckpt_01000.pt) andtokenizer.pklare incheckpoints_v2/and are tracked by Git (you may need to force-add them or update.gitignore). - If using RAG, ensure
rag_index/chroma.sqlite3is committed. - Push your repository to GitHub.
- Go to share.streamlit.io and click New app.
- Select your GitHub repository, branch, and set the Main file path to
app.py. - Click Deploy. Streamlit Cloud will automatically install dependencies from
requirements.txtand launch the app.
_pickle.UnpicklingErrorwhen loading checkpoint: Recent versions of PyTorch default toweights_only=Truefor security. Because our checkpoint contains a customGPTConfigobject, ensure you usetorch.load(..., weights_only=False)inapp.py.- App crashes on memory on Streamlit Cloud: The free tier has a 1GB memory limit. Ensure you are using the
--preset tinycheckpoint (~3.5M params) rather than the default 10M parameter model, which might spike RAM during generation. - Missing Tokenizer Error: The tokenizer is saved separately from the checkpoint (
tokenizer.pkl). Ensure both files are committed in the same directory. - RAG takes a long time on first load:
sentence-transformersandchromadbare heavy dependencies. The very first load on Streamlit Cloud might take a minute or two as it downloads the embedding models.
This project is licensed under the MIT License - see the LICENSE file for details.

