# GPU Throughput Analysis: Nano-GPT2 vs Transformer Training on 1x H800 GPU, the Nano-GPT2 model achieves ~180,000 tokens/sec while the Transformer (Attention Is All You Need) achieves ~50,000 tokens/sec. This document analyzes why. ## Key Differences | | Nano-GPT2 | Transformer | |---|---|---| | Architecture | Decoder-only | Encoder-Decoder | | Attention | `F.scaled_dot_product_attention` (Flash Attention) | Manual matmul + softmax | | Tokens/micro-batch | 64 x 1024 = 65,536 | ~15,388 | | Attention ops/layer | 1 (self-attn) | 3 (enc self-attn + dec self-attn + cross-attn) | | Optimizer | Fused AdamW | Regular Adam | | Sequence length | 1024 (fixed, dense) | ~50 avg (padded to max in batch) | ### Root Causes (biggest to smallest) 1. **No Flash Attention** -- The biggest factor. The Transformer manually materializes the full (T,T) attention matrix. GPT-2 uses `F.scaled_dot_product_attention` which dispatches to Flash Attention on CUDA -- 2-4x faster and far more memory efficient. 2. **Low GPU utilization** -- Only 15K tokens per accumulation step vs GPT-2's 65K per micro-batch. The H800 is starved for work. Short WMT sentences (~30-50 tokens) with padding waste compute. 3. **Encoder-Decoder = more ops per token** -- Each decoder layer has 3 attention operations vs GPT-2's 1. Roughly 2x more attention compute for the same number of layers. 4. **No fused optimizer** -- GPT-2 uses `fused=True` on AdamW, which runs the optimizer step in a single CUDA kernel. --- ## Parameter Count ### Nano-GPT2 (Decoder-only) Config: `n_layer=12, n_head=12, n_embd=768, vocab_size=50304` | Component | Calculation | Params | |---|---|---| | Token embedding | 50304 x 768 | 38,633,472 | | Position embedding | 1024 x 768 | 786,432 | | **Per block (x12):** | | | | LN1 | 768 + 768 | 1,536 | | QKV projection (fused) | 768 x 2304 + 2304 | 1,771,776 | | Attn output projection | 768 x 768 + 768 | 590,592 | | LN2 | 768 + 768 | 1,536 | | MLP up (c_fc) | 768 x 3072 + 3072 | 2,362,368 | | MLP down (c_proj) | 3072 x 768 + 768 | 2,360,064 | | **Block subtotal** | | **7,087,872** | | 12 blocks total | 7,087,872 x 12 | 85,054,464 | | Final LN | 768 + 768 | 1,536 | | lm_head | tied with token embedding | 0 | | **Total** | | **124,475,904 (~124M)** | ### Transformer (Encoder-Decoder) Config: `N=6, h=8, d_model=512, d_ff=2048, vocab=37000, shared_vocab=True` Shared embedding: weight tied between src_embed, tgt_embed, and generator. | Component | Calculation | Params | |---|---|---| | Shared embedding | 37000 x 512 | 18,944,000 | | Positional encoding | sinusoidal (buffer, not learned) | 0 | | **Per encoder layer (x6):** | | | | Self-attn: 4 x Linear(512,512) | 4 x (512 x 512 + 512) | 1,050,624 | | FFN w1 | 512 x 2048 + 2048 | 1,050,624 | | FFN w2 | 2048 x 512 + 512 | 1,049,088 | | 2 x LayerNorm(512) | 2 x (512 + 512) | 2,048 | | **Encoder layer subtotal** | | **3,152,384** | | 6 encoder layers | 3,152,384 x 6 | 18,914,304 | | Encoder final LN | | 1,024 | | **Per decoder layer (x6):** | | | | Masked self-attn: 4 x Linear | 4 x (512 x 512 + 512) | 1,050,624 | | Cross-attn: 4 x Linear | 4 x (512 x 512 + 512) | 1,050,624 | | FFN w1 + w2 | (same as encoder) | 2,099,712 | | 3 x LayerNorm(512) | 3 x 1024 | 3,072 | | **Decoder layer subtotal** | | **4,204,032** | | 6 decoder layers | 4,204,032 x 6 | 25,224,192 | | Decoder final LN | | 1,024 | | Generator | tied with embedding | 0 | | **Total** | | **63,084,544 (~63M)** | --- ## FLOPs Per Token (Forward Pass) For matrix multiply: FLOPs = 2 x M x N x K. Using `d = d_model`, `T = seq_len`. ### Nano-GPT2 -- per token, per layer | Op | FLOPs | d=768, T=1024 | |---|---|---| | QKV projection | 2 x d x 3d = 6d^2 | 3,538,944 | | Attention (QK^T + attn x V) | 2 x 2 x T x d = 4Td | 3,145,728 | | Attn output projection | 2d^2 | 1,179,648 | | MLP up + down | 2 x 2 x d x 4d = 16d^2 | 9,437,184 | | **Per layer** | **24d^2 + 4Td** | **17,301,504** | - 12 layers: **207,618,048** - lm_head (2 x d x V): 77,266,944 - **Forward total: ~285M FLOPs/token** - **Training (~3x fwd): ~855M FLOPs/token** ### Transformer -- per token pair (1 src + 1 tgt), per layer Assuming T_src ~ T_tgt ~ T ~ 40 (typical WMT sentence length). **Encoder layer** (processes src token): | Op | FLOPs | |---|---| | Self-attn QKV + output | 8d^2 | | Self-attn compute | 4 x T_src x d | | FFN | 16d^2 | | **Per encoder layer** | **24d^2 + 4Td** | **Decoder layer** (processes tgt token): | Op | FLOPs | |---|---| | Masked self-attn (QKV + out) | 8d^2 | | Self-attn compute | 4 x T_tgt x d | | Cross-attn (QKV + out) | 8d^2 | | Cross-attn compute | 4 x T_src x d | | FFN | 16d^2 | | **Per decoder layer** | **32d^2 + 4d(T_src + T_tgt)** | Per layer pair (1 enc + 1 dec): **56d^2 + 4d(2T_src + T_tgt)** With d=512, T=40: | | Per layer | 6 layers | |---|---|---| | Encoder layer | 24 x 512^2 + 4 x 40 x 512 = 6,373,376 | 38,240,256 | | Decoder layer | 32 x 512^2 + 4 x 512 x 80 = 8,552,448 | 51,314,688 | | Generator (2 x d x V) | | 37,888,000 | | **Total forward** | | **127,442,944 (~127M)** | | **Training (~3x)** | | **~382M FLOPs/token-pair** | --- ## Summary | | Nano-GPT2 | Transformer | Ratio | |---|---|---|---| | Parameters | **124M** | **63M** | 2.0x | | Layers | 12 decoder | 6 enc + 6 dec | -- | | Attention ops/layer | 1 (self) | 1 (enc) + 2 (dec) = 3 | 3x | | d_model | 768 | 512 | 1.5x | | Seq length | 1024 | ~40 | 25x | | FLOPs/token (fwd) | **285M** | **127M** (per src+tgt pair) | 2.2x | GPT-2 does ~2.2x more FLOPs per token -- yet it achieves 3.6x higher throughput. This confirms the gap is **not** from model complexity but from **implementation efficiency** (manual attention vs Flash Attention, low batch utilization, no fused optimizer).