ScratchLM-95M
A 95-million-parameter transformer language model built entirely from scratch in PyTorch β no pretrained weights, no from_pretrained. Trained on WikiText-103 and fine-tuned for question answering using two-stage LoRA.
Beats GPT-3 Small (PPL 26.0) on WikiText-103 with 30% fewer parameters.
Model Details
| Property | Value |
|---|---|
| Parameters | ~95M (88.7M base + embedding table) |
| Architecture | 12-layer causal transformer |
| Position Encoding | RoPE (Rotary Position Embedding) |
| FFN Activation | SwiGLU |
| Attention | Causal + Flash Attention fallback |
| Context Length | 512 tokens |
| Tokenizer | GPT-2 BPE (vocab size 50,257) |
| Framework | PyTorch (from scratch) |
| Base Pretraining | WikiText-103 |
| Fine-tuning Method | Two-stage LoRA (R128 β R64) |
| Trainable Params (LoRA) | ~1.6M (1.8% of base) |
Model Configuration
VOCAB_SIZE = 50257 # GPT-2 tokenizer
MAX_SEQ_LEN = 512 # Context window
EMBED_DIM = 512 # Model dimension
NUM_HEADS = 8 # Attention heads
NUM_LAYERS = 12 # Transformer blocks
D_FF = 2730 # SwiGLU intermediate dim
DROPOUT = 0.1
Performance
| Checkpoint | PPL |
|---|---|
| Base model (WikiText-103, Epoch 17) | 24.40 |
| GPT-3 Small (reference) | 26.0 |
| Stage 1 LoRA β R128, Ξ±=256, Epoch 7 | 21.48 |
| Stage 2 LoRA β R64, Ξ±=128, Epoch 2 | 20.83 |
- Total PPL improvement: 3.57 points (24.40 β 20.83)
- Train/Val gap: 0.1216 (healthy, no overfitting)
- Fine-tune hardware: Single RTX 5050 (8.5GB VRAM), ~6 hours
Training Details
Base Pretraining (V3)
| Hyperparameter | Value |
|---|---|
| Dataset | WikiText-103 |
| Effective batch size | 32 (8 Γ 4 grad accum) |
| Learning rate | 2e-4 (cosine decay) |
| Warmup steps | 800 |
| Optimizer | AdamW (Ξ²1=0.9, Ξ²2=0.95) |
| Gradient clipping | 0.8 |
| Best checkpoint | Epoch 17 |
Fine-Tuning (Two-Stage LoRA)
Full fine-tuning (all 95M params) caused catastrophic forgetting β PPL rose from 24.40 to 35+. LoRA was adopted to freeze 99% of weights.
LoRA config:
LORA_R = 32 # Final rank
LORA_ALPHA = 64 # Scaling factor (2Γ rank)
LORA_DROPOUT = 0.05
# Target modules: qkv_proj, out_proj
Two-stage strategy:
- Stage 1 β LoRA R=128, Ξ±=256 β best PPL 21.48 at Epoch 7
- Stage 2 β Merge Stage 1 weights into base, restart with LoRA R=64, Ξ±=128 β best PPL 20.83 at Epoch 2
Weight merge equation:
W_merged = W_base + (B @ A) * (alpha / rank)
Dataset Engineering
355k clean QA pairs from three sources, after rejecting 460k noisy alternatives:
| Source | Pairs | Notes |
|---|---|---|
| FreebaseQA / TriviaQA | 205k | Factual pairs |
| ELI5 (cleaned) | 90k | Reddit markdown stripped |
| SciQ | 60k | Science explanations |
Cleaning steps: regex removal of filler phrases ("the correct answer is..."), Reddit markdown stripping, deduplication, length filtering (20β80 word answers), proper punctuation enforcement. ~22,000 entries removed.
Usage
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
tokenizer = AutoTokenizer.from_pretrained("Debarun12/ScratchLM-95M")
model = AutoModelForCausalLM.from_pretrained("Debarun12/ScratchLM-95M")
model.eval()
prompt = "What is the official language of Brazil?"
inputs = tokenizer(prompt, return_tensors="pt")
with torch.no_grad():
output = model.generate(
**inputs,
max_new_tokens=100,
repetition_penalty=3.0,
do_sample=False
)
print(tokenizer.decode(output[0], skip_special_tokens=True))
# β "The official language is Portuguese, used in national and international contexts."
Note: A repetition penalty of 3.0 (window 128 tokens) is strongly recommended β it is required to suppress repetition loops at this model scale.
Limitations
- Hallucinates facts for questions outside training data coverage (e.g., precise dates, counts)
- Struggles with exact numerical facts
- 95M parameter size caps final PPL around 20β22 without architectural scaling
- Context window limited to 512 tokens
Key Engineering Decisions
Why RoPE? Superior relative position awareness over learned absolute embeddings; no position table to overfit. Same choice as LLaMA, Mistral, and Gemma.
Why SwiGLU? Gated linear unit consistently outperforms GELU in practice; requires 3 linear layers (w1, w2, w3).
Why LoRA over full fine-tune? Full fine-tune destroyed base knowledge (PPL 24.40 β 35+) due to domain shift between long encyclopedia text and short factual QA. LoRA preserves base weights while adapting ~1.8% of parameters.
Why two-stage LoRA? Stage 1 (R128) showed widening train/val gap at Epoch 7. Merging and restarting with lower rank (R64) gave a fresh second stage without continued overfitting.
Citation
If you use this model or find this work helpful, please cite:
@misc{scratchlm95m,
author = {Debarun Das},
title = {ScratchLM-95M: A 95M Parameter Language Model Built From Scratch},
year = {2025},
publisher = {HuggingFace},
url = {https://huggingface.co/Debarun12/ScratchLM-95M}
}
Author
Debarun Das
- GitHub: github.com/Debarun12
- HuggingFace: huggingface.co/Debarun12