A decoder-only transformer built up from scratch in PyTorch, trained on WikiText-103. Dense and Mixture-of-Experts variants, param-matched so the two are directly comparable.
- Pre-Norm (
x + Attn(Norm(x)),x + FFN(Norm(x))), RMSNorm, SwiGLU feedforward bias=Falseon all linear projections, weight tying between embedding and lm_head- GPT-2 (tiktoken) tokenizer
- Grouped-query attention (fewer KV heads than query heads) with a working KV cache for incremental decoding
- Mixture-of-Experts: router + per-expert FFN, Switch-style load-balancing aux loss, expert-capacity capping (overflow tokens dropped by lowest router confidence), grouped-GEMM dispatch (one batched matmul instead of a per-expert loop)
- Mixed precision +
torch.compile()training, AdamW
transformers_from_scratch/attention.py— multi-head attention, GQA, KV cachetransformers_from_scratch/layers.py— RMSNorm, SwiGLU, dense decoder layer;NaiveMoEDecoderLayer(masked-loop dispatch, kept as the readable reference) andMoEDecoderLayer(capacity-capped, grouped-GEMM dispatch)transformers_from_scratch/transformer.py— decoder-only model, dense or MoEtransformers_from_scratch/data/— WikiText-103 loading and batchingtrain.py/utils.py— training loop, mixed precision, checkpointingtests/— KV cache and GQA correctness (cached generation matches full recompute)
uv sync
# Dense
uv run python train.py
# MoE
uv run python train.py --moeHyperparameters are arguments to train() in train.py.
make test- Static-shaped MoE dispatch (scatter/gather instead of boolean masking) so
torch.compilecovers the grouped-GEMM path