Encoder-decoder Transformers built from scratch in PyTorch (no
nn.Transformer, no nn.MultiheadAttention, no fused SDPA), trained to decrypt
binary cipher sequences into English plaintext, with a controlled
five-configuration ablation study. The BPE tokenizer is also implemented from
scratch, with no tokenizer library.
Course: Advanced NLP, IIIT Hyderabad. Roll number 2023111026.
Task
5000 aligned line pairs. The cipher is a repeating-key XOR:
int(cipher[8i:8i+8], 2) == ord(plain[i]) ^ KEY[i % 8] with KEY = b"ANLP2026",
verified on all 5000 lines. Because the key is position-dependent, the task is
alignment-heavy: the model must learn which source region each output position
reads.
Configurations
Each of C2-C5 changes exactly one component from C1. All share d_model 256,
8 heads, 4+4 layers, FFN 1024, dropout 0.1, AdamW lr 3e-4 (1000 warmup, cosine),
label smoothing 0.1, bf16, 200 epochs with early stopping, identical splits/seeds.
Config
Positional
Attention
Norm
Tokenization
C1-base
Sinusoidal
MHA
LayerNorm
BPE subword
C2-rope
RoPE
MHA
LayerNorm
BPE subword
C3-gqa
Sinusoidal
GQA (8Q/2KV)
LayerNorm
BPE subword
C4-rmsnorm
Sinusoidal
MHA
RMSNorm
BPE subword
C5-blt
Sinusoidal
MHA
LayerNorm
BLT (token-free)
C1-C4 use a from-scratch BPE on both sides: vocab 1024 over the raw 0/1
ciphertext, 8000 over the plaintext, each trained on the training split only.
C5 uses no vocabulary. Every 8 cipher bits are grouped into one byte value
(0-255), and patch boundaries are placed by entropy, following the BLT paper:
an order-2 byte n-gram model with backoff estimates H(next byte | context), and a
new patch starts where the next byte is hard to predict. The threshold sits at the
75th percentile of the training entropy distribution (3.958 bits); 99.6% of
boundaries come from entropy, and patches run 1-12 bytes averaging 4.0. Variable
patches are pooled by cross-attention with a learned query.
Results (test split, greedy decoding, line-level)
Config
Bit acc.
Seq. acc.
Levenshtein
BLEU
ROUGE-1
ROUGE-2
ROUGE-L
Val loss
C1-base
0.712
0.004
75.91
61.74
0.786
0.652
0.785
1.238
C2-rope
0.780
0.046
19.30
84.31
0.920
0.859
0.920
0.497
C3-gqa
0.704
0.006
86.24
59.03
0.770
0.633
0.768
1.281
C4-rmsnorm
0.707
0.000
80.45
60.30
0.779
0.642
0.777
1.271
C5-blt
0.967
0.364
5.31
-
-
-
-
0.088
BLEU/ROUGE apply to tokenized models only. Mean test line is ~598 characters, so
Levenshtein 5.31 is ~99.1% of characters correct.
Two findings worth noting. RoPE is the largest single-component win among the
tokenized models, the opposite of what fixed-width tokenization showed earlier:
with subword segmentation the source-to-target correspondence is a
variable-length span relation, which relative offsets suit better than absolute
positions. C5 dominates overall, reconstructing more than a third of test
lines exactly.
Training efficiency
Params
s/epoch
Samples/s
Tokens/s
Peak GPU mem
Best epoch
C1-base
9.68M
17.2
665.7
31.7k
3.00 GB
192
C5-blt
12.25M
65.9
174.3
36.8k
4.92 GB
80
C5 costs ~3.8x more per step but pushes more raw tokens per second, locating the
cost in sequence length rather than model speed.
Each checkpoint is a dict with model, cfg, hp, epoch, val_loss.
Loading requires the model classes from the assignment code.
Hardware: one NVIDIA GB10 (DGX Spark), PyTorch 2.13 + CUDA 13. Lines are split
into phase-aligned 256-character windows for training and decoding; decoded
windows are rejoined into full lines before metrics are computed.