Code Architecture: LoRA Notebook
Overview
The notebook implements LoRA (Low-Rank Adaptation) from scratch in PyTorch, demonstrating the technique on a small Transformer model for a text classification task. It compares full fine-tuning vs. LoRA fine-tuning in terms of trainable parameters, training speed, and final accuracy.
Section-by-Section Breakdown
1. Setup & Imports
- Import PyTorch, torch.nn, and standard utilities.
- Set random seeds for reproducibility.
- Configure device (CPU/GPU).
2. Toy Dataset Preparation
- Create a small synthetic text classification dataset (e.g., sentiment-like binary classification with short sequences).
- Build a simple vocabulary and tokenize inputs into integer ID sequences.
- Create train/validation DataLoader objects.
- Data shapes: Input tensors are
(batch_size, seq_len)with integer token IDs; labels are(batch_size,).
3. Small Transformer Model (Pretrained Base)
- Implement a minimal Transformer encoder from scratch: token embeddings + positional encoding + N Transformer encoder layers (multi-head self-attention + feedforward + residual connections + layer norm).
- Key class:
SimpleTransformer(nn.Module)with configurabled_model,n_heads,n_layers,vocab_size,num_classes. - This simulates the “pre-trained” model — we initialize it and do a light pre-training pass so it has non-random weights, then freeze it.
- Forward pass shapes:
(batch, seq_len)→ embedding(batch, seq_len, d_model)→ encoder layers(batch, seq_len, d_model)→ pooled → classifier(batch, num_classes).
4. LoRA Layer Implementation
- Key class:
LoRALinear(nn.Module)— wraps a frozennn.Linearlayer and adds trainable low-rank matrices A and B.- Original weight:
W₀(frozen,requires_grad=False) - LoRA matrices:
A(r×k),B(d×r) — trainable - Initialization: A ~ N(0, σ²), B = 0
- Forward:
h = W₀·x + (α/r)·B·A·x merge()method: computesW = W₀ + (α/r)·B·Aand replaces the frozen weight (for zero-latency inference).
- Original weight:
- Shapes: If original Linear is
(d_model, d_model), then A is(r, d_model), B is(d_model, r), with r = 4 or 8.
5. Injecting LoRA into the Transformer
- Function
apply_lora_to_model(model, r, alpha, target_modules)that replaces specified Linear layers (Wq, Wv) with LoRALinear wrappers. - The original model parameters are frozen (
requires_grad=False), only LoRA A/B matrices are trainable. - Function
count_trainable_parameters(model)to report the difference.
6. Full Fine-Tuning Baseline
- Clone the pre-trained model, keep all parameters trainable.
- Train with Adam optimizer on the classification task for N epochs.
- Record training loss, validation accuracy, and wall-clock time.
7. LoRA Fine-Tuning
- Apply LoRA to the pre-trained model (r=4, α=4, on Wq and Wv).
- Train only the LoRA parameters with Adam on the same task for the same N epochs.
- Record training loss, validation accuracy, and wall-clock time.
8. Comparison & Analysis
- Compare: total parameters, trainable parameters (full FT vs LoRA), training time, final accuracy.
- Print a summary table.
- Demonstrate LoRA merge: call
merge()on the LoRA model and verify forward pass produces identical outputs.
9. Rank Ablation (Optional)
- Try r = 1, 2, 4, 8, 16 and plot validation accuracy vs. rank.
- Shows that even r=1 captures significant adaptation signal.
Key Functions/Classes
| Name | Type | Purpose |
|---|---|---|
LoRALinear | nn.Module | Wraps frozen Linear + trainable low-rank A, B matrices |
SimpleTransformer | nn.Module | Minimal Transformer encoder for classification |
apply_lora_to_model | function | Replaces target Linear layers with LoRALinear |
count_trainable_parameters | function | Counts params with requires_grad=True |
merge_lora_weights | function | Merges BA into W₀ for zero-latency inference |
Data Flow
Token IDs (batch, seq_len)
→ Embedding (batch, seq_len, d_model)
→ Positional Encoding
→ Transformer Encoder Layer × N
→ Multi-Head Attention
→ Wq, Wk, Wv, Wo projections [LoRA applied to Wq, Wv]
→ Scaled dot-product attention
→ Feedforward (2-layer MLP)
→ Residual + LayerNorm
→ Mean pooling (batch, d_model)
→ Classifier head (batch, num_classes)
→ Cross-entropy loss
Deliberate Simplifications vs. Full Paper
- Model scale: We use a tiny Transformer (d_model=64, 2 layers, 2 heads, vocab ~500) instead of GPT-3 175B or RoBERTa-large. The principle is identical.
- Pre-training: We do a brief pre-training pass on the toy dataset rather than large-scale pre-training. The focus is on demonstrating the LoRA mechanism, not achieving SOTA.
- Task scope: Single binary classification task instead of GLUE/WikiSQL/SAMSum benchmarks.
- LoRA targets: We apply LoRA to Wq and Wv only (matching the paper’s recommended configuration), but don’t explore all combinations (Wq+Wk+Wv+Wo, MLP, etc.).
- No distributed training: Single GPU/CPU, no model parallelism or gradient checkpointing.
- No quantization: We use full fp32 weights; QLoRA and 8-bit variants are out of scope.