TinyGPT is an educational, end-to-end decoder-only GPT implementation built from first principles with PyTorch.
The project is designed to make the complete lifecycle of a modern language model understandable and inspectable — from raw text and tokenization, through Transformer internals and pretraining, to supervised fine-tuning, FastAPI model serving, streaming responses, conversation history, and a ChatGPT-like web interface.
Goal: build a small GPT-style language model that runs on a normal CPU while preserving the same major architectural ideas used by much larger LLM systems.
The project includes a browser-based ChatGPT-like interface connected directly to the TinyGPT/TinyChatGPT FastAPI backend.
The UI demonstrates the complete inference path from a browser message to FastAPI, conversation handling, prompt construction, tokenization, autoregressive Transformer generation, streaming, and incremental rendering in the browser.
Raw Text
↓
Dataset Preparation
↓
Byte-Level BPE Tokenizer
↓
Token IDs
↓
Token Embeddings
↓
RoPE Positional Encoding
↓
Causal Multi-Head Self-Attention
↓
RMSNorm
↓
SwiGLU Feed-Forward Network
↓
Transformer Blocks
↓
Decoder-Only TinyGPT
↓
Cross-Entropy Language-Model Loss
↓
Backpropagation + AdamW
↓
Pretraining
↓
Checkpointing + Evaluation
↓
Autoregressive Text Generation
↓
Supervised Fine-Tuning (SFT)
↓
TinyChatGPT
↓
FastAPI Backend
↓
Conversation History
↓
Streaming Responses
↓
ChatGPT-Like Web UI
Large language models are often used through high-level libraries and APIs, which can hide what is happening internally.
TinyGPT intentionally implements the important pieces directly so that you can inspect:
- how text becomes tokens;
- how token IDs become vectors;
- how Query, Key, and Value projections work;
- how causal self-attention prevents future-token access;
- how RoPE adds positional information;
- how RMSNorm and residual connections stabilize Transformer blocks;
- how SwiGLU performs per-token computation;
- how the language-model head produces next-token logits;
- how cross-entropy trains next-token prediction;
- how gradients are computed and parameters are updated;
- how checkpoints make training resumable;
- how autoregressive generation repeatedly predicts one token at a time;
- how instruction fine-tuning converts a base language model into a chat-style model;
- how padding masks and assistant-only loss masking work;
- how gradient accumulation simulates a larger effective batch;
- how a trained model is exposed through FastAPI;
- how multi-turn conversation history is constructed;
- how generated text is streamed to a browser in real time.
The current small-model configuration is approximately:
| Component | Value |
|---|---|
| Context length | 128 tokens |
Embedding dimension (d_model) |
128 |
| Attention heads | 4 |
| Head dimension | 32 |
| Transformer layers | 4 |
Feed-forward dimension (d_ff) |
384 |
| Tokenizer vocabulary | ~1024 |
| Parameter scale | ~1 million |
The model is intentionally small enough to train on a CPU while still containing the major components of a decoder-only Transformer.
Input
│
├───────────────┐
│ │
RMSNorm │
│ │
Multi-Head │
Causal Attention │
│ │
└──── Add ──────┘
│
├───────────────┐
│ │
RMSNorm │
│ │
SwiGLU │
│ │
└──── Add ──────┘
│
Output
The full model is:
Token IDs [B, T]
↓
Token Embedding
↓
Transformer Block × N
↓
Final RMSNorm
↓
LM Head
↓
Logits [B, T, V]
The embedding matrix and language-model output head are weight-tied.
X [B, T, C]
↓
Fused QKV Projection
↓
Q, K, V [B, H, T, D]
↓
RoPE applied to Q and K
↓
Q × Kᵀ
↓
Scaled Attention Scores [B, H, T, T]
↓
Causal Mask
↓
Optional Padding-Key Mask
↓
Softmax
↓
Weighted Sum of V
↓
Merge Heads
↓
Output Projection
↓
[B, T, C]
TinyGPT includes a custom byte-level BPE tokenizer.
Key properties:
- base symbols are raw byte values;
- frequent byte/token pairs are merged during tokenizer training;
- the tokenizer supports arbitrary UTF-8 input;
- an end-of-text token marks document boundaries;
- tokenizer artifacts are saved and fingerprinted so checkpoints can validate tokenizer compatibility.
The base model is trained using causal next-token prediction.
For a sequence:
[token_1, token_2, token_3, token_4, token_5]
the training pair is:
Input:
[token_1, token_2, token_3, token_4]
Target:
[token_2, token_3, token_4, token_5]
Training includes:
- AdamW;
- decay/no-decay parameter groups;
- gradient clipping;
- warmup;
- cosine learning-rate decay;
- train/validation evaluation;
- deterministic validation sampling;
- best/latest checkpoints;
- RNG-state checkpointing.
After pretraining, the base model can continue text but does not naturally behave like an assistant.
SFT teaches chat-style behavior using prompts such as:
### System:
You are a helpful and concise assistant.
### User:
What is 2 + 2?
### Assistant:
2 + 2 equals 4.
Only assistant-response tokens contribute directly to the SFT loss. System and user tokens remain in context but are masked from the loss using IGNORE_INDEX = -100.
The SFT pipeline also supports:
- variable-length examples;
- dynamic right padding;
- attention masks;
- assistant-only target masking;
- token-weighted evaluation;
- mini-batching;
- gradient accumulation.
TinyGPT is an educational model, not a production-scale frontier LLM.
The current base model is trained on a relatively small TinyStories subset and contains roughly one million parameters. The initial SFT dataset is intentionally tiny and was created primarily to validate the full training and serving pipeline.
As a result:
- story-like next-token prediction can work;
- general chat quality is limited;
- factual knowledge is limited;
- reasoning ability is limited;
- multi-turn instruction following is still weak;
- small SFT datasets can cause overfitting or catastrophic forgetting.
The architecture is working; model quality depends heavily on model scale, pretraining data, instruction data, and training duration.
tinygpt/
├── configs/
├── data/
│ ├── raw/
│ ├── processed/
│ ├── tokenizer/
│ ├── tokens/
│ └── instruction/
├── tinygpt/
│ ├── api/
│ │ ├── __init__.py
│ │ ├── app.py
│ │ ├── schemas.py
│ │ ├── model_service.py
│ │ └── conversation_service.py
│ ├── data/
│ ├── generation/
│ │ ├── chat.py
│ │ ├── generate.py
│ │ ├── load.py
│ │ ├── sampling.py
│ │ └── stream.py
│ ├── model/
│ │ ├── attention.py
│ │ ├── block.py
│ │ ├── embeddings.py
│ │ ├── gpt.py
│ │ ├── mlp.py
│ │ ├── norm.py
│ │ ├── rope.py
│ │ └── transformer.py
│ ├── sft/
│ ├── tokenizer/
│ ├── training/
│ └── utils/
├── frontend/
│ ├── index.html
│ ├── styles.css
│ └── app.js
├── scripts/
├── tests/
├── checkpoints/
├── requirements.txt
├── .gitignore
└── README.md
Recommended:
- Windows 11, Linux, or macOS
- Python 3.11+
- 16 GB+ RAM recommended
- CPU is sufficient for the default small model
- GPU is optional
The project has been developed and tested on a CPU-only Windows environment.
If the project is already on your machine:
cd C:\path\to\tinygptIf hosting it on GitHub:
git clone <YOUR_REPOSITORY_URL>
cd tinygptpython -m venv .venv
.\.venv\Scripts\Activate.ps1python -m venv .venv
source .venv/bin/activatepython -m pip install --upgrade pip
pip install -r requirements.txtMake sure FastAPI and Uvicorn are available:
pip install fastapi uvicornIf you use the TinyStories download script, the Hugging Face datasets package is also required.
python -m scripts.check_environment
python -m scripts.check_torchFor a CPU machine, device: cpu is expected.
python -m scripts.download_tinystoriespython -m scripts.prepare_datapython -m scripts.train_tokenizerTokenizer artifacts are written under:
data/tokenizer/
python -m scripts.prepare_tokensIf you retrain the tokenizer, regenerate tokenized datasets before training the model again.
python -m scripts.trainCheckpoints are written under an experiment directory similar to:
checkpoints/
└── tinystories_5mb_v1/
├── best.pt
├── latest.pt
└── run_metadata.json
latest.pt is intended for resuming training. best.pt represents the best validation checkpoint.
Depending on the scripts present in the repository:
python -m scripts.evaluate_untrained
python -m scripts.evaluate_best
python -m scripts.compare_checkpoints
python -m scripts.inspect_next_token
python -m scripts.evaluate_generationsA useful clean test prompt is:
Once upon a time there was a little
The base checkpoint should generally perform better on story-style continuation than on arbitrary chat or technical prompts.
python -m scripts.prepare_sft_dataInstruction files are stored under:
data/instruction/
├── train.jsonl
├── val.jsonl
└── test.jsonl
python -m scripts.train_sftA chat experiment produces checkpoints similar to:
checkpoints/
└── tinychat_sft_v1/
├── best_chat.pt
└── latest_chat.pt
Later experiments may use names such as:
tinychat_sft_batched_v1
tinychat_sft_accum_v1
Use the checkpoint that actually exists in your environment.
Examples:
python -m scripts.check_multihead_attention
python -m scripts.check_transformer_block
python -m scripts.check_transformer_stack
python -m scripts.check_tinygpt
python -m scripts.check_model_causality
python -m scripts.check_sft_collate
python -m scripts.check_padding_attention
python -m scripts.check_batched_sft_step
python -m scripts.check_gradient_accumulationIf available:
python -m scripts.chat_onceor:
python -m scripts.chatThe Transformer itself is stateless between requests. Multi-turn behavior is created by rebuilding the prompt from conversation history.
The server:
- loads the model once during application startup;
- keeps the model and tokenizer in memory;
- exposes health and chat endpoints;
- maintains in-memory conversation history;
- constructs chat context;
- supports standard and streaming inference.
In:
tinygpt/api/app.py
make sure CHECKPOINT_PATH points to a checkpoint that exists, for example:
CHECKPOINT_PATH = (
"checkpoints/"
"tinychat_sft_v1/"
"best_chat.pt"
)uvicorn tinygpt.api.app:app --host 127.0.0.1 --port 8000Expected startup:
Loading TinyChatGPT...
TinyChatGPT loaded.
Application startup complete.
GET /healthPOST /chatRequest:
{
"message": "Hello"
}POST /conversationsGET /conversations/{conversation_id}POST /conversations/{conversation_id}/messagesPOST /conversations/{conversation_id}/messages/streamThe response is streamed as SSE-style events:
event: token
data: {"type":"token","text":"Hello"}
event: token
data: {"type":"token","text":" there"}
event: done
data: {"type":"done","conversation_id":"..."}
Start the backend:
uvicorn tinygpt.api.app:app --host 127.0.0.1 --port 8000Then open:
http://127.0.0.1:8000/
The UI supports:
- creating a new conversation;
- entering user messages;
- displaying user and assistant message bubbles;
- progressive streaming of generated output;
- reusing a conversation ID for multi-turn context.
Keyboard
↓
HTML / JavaScript
↓
HTTP POST
↓
Uvicorn
↓
FastAPI
↓
Pydantic Validation
↓
Conversation Lookup
↓
Chat Prompt Construction
↓
BPE Tokenizer
↓
Token IDs
↓
TinyGPT
↓
Embeddings
↓
RoPE
↓
Q / K / V
↓
Causal Multi-Head Attention
↓
RMSNorm
↓
SwiGLU
↓
Transformer Blocks
↓
LM Head
↓
Next-Token Logits
↓
Temperature / Top-K / Top-P
↓
Sample Next Token
↓
Decode
↓
Stream HTTP Chunk
↓
Browser ReadableStream
↓
DOM Update
TinyGPT separates three kinds of memory:
Stored in model weights such as best_chat.pt.
The prompt tokens currently visible to the Transformer, limited by context_length.
Conversation messages stored by the FastAPI backend.
The current implementation stores conversations in Python memory, so they are lost when the backend process restarts.
Small today, scalable tomorrow.
The project keeps architecture separate from infrastructure:
- tokenizer;
- data preparation;
- model;
- training;
- evaluation;
- generation;
- SFT;
- API serving;
- frontend.
The goal is to implement transparent reference versions first and progressively introduce optimizations.
- larger and more diverse pretraining data;
- larger instruction datasets;
- stronger multi-turn SFT;
- persistent conversation storage;
- conversation sidebar/history;
- proper chat special tokens;
- sequence packing;
- KV cache;
- faster autoregressive inference;
- improved request concurrency;
- model evaluation benchmarks;
- LoRA / parameter-efficient fine-tuning;
- preference optimization concepts;
- tool calling;
- retrieval-augmented generation;
- GPU / mixed-precision training;
- distributed training concepts;
- production observability and deployment.
TinyGPT is useful for learning:
- NLP fundamentals;
- Transformer internals;
- GPT architecture;
- PyTorch;
- training loops;
- optimization;
- tokenization;
- language-model evaluation;
- instruction fine-tuning;
- LLM inference;
- API model serving;
- streaming;
- full-stack AI application architecture.
OS: Windows 11
Python: 3.14.x
PyTorch: CPU build
Device: CPU
RAM: 32 GB
A GPU is not required for the default TinyGPT configuration.
TinyGPT is an educational implementation. It is not intended to match the quality, knowledge, safety systems, scale, latency, or reliability of production frontier language models.
Its purpose is to make the core engineering ideas behind GPT-style systems visible and understandable in one complete codebase.
Raw Text
↓
Tokenizer
↓
Transformer
↓
Pretraining
↓
Next-Token Prediction
↓
Instruction Fine-Tuning
↓
Chat Model
↓
FastAPI
↓
Streaming
↓
Web UI
The entire stack is intentionally small enough to inspect, modify, train, and run locally.
