A clean, minimal implementation of Masked Diffusion Models for language modeling.
model.py # Bidirectional transformer (RoPE, RMSNorm, SwiGLU)
diffusion.py # Forward masking + reverse sampling
data.py # Data loading (nanoGPT-style binary format)
train.py # Training script (Hydra + wandb)
sample.py # Text generation
eval.py # Evaluation (loss, perplexity)
configs/
config.yaml # Default configuration
experiment/ # Experiment configs (small, medium)
# Install dependencies
pip install torch numpy hydra-core omegaconf wandb
# Train (auto-downloads Shakespeare)
python train.py
# Generate text (checkpoint saved in outputs/{date}/{time}/checkpoints/)
python sample.py checkpoint=outputs/2024-01-01/12-00-00/checkpoints/ckpt.pt prompt="ROMEO:"
# Evaluate
python eval.py checkpoint=outputs/2024-01-01/12-00-00/checkpoints/ckpt.pt
The default dataset is Tiny Shakespeare (~1MB, 1.1M characters), automatically downloaded on first run.
| Split | Characters | Tokens |
|---|---|---|
| Train | ~1M | ~1M |
| Val | ~100K | ~100K |
| Vocab | 65 unique characters | - |
Data format (nanoGPT-style):
train.bin, val.bin: Binary files of uint16 token IDsmeta.pkl: Vocabulary metadata (stoi, itos, vocab_size)Preparation (automatic or manual):
# Auto-downloads and prepares data on first train.py run
# Or manually:
python -c "from data import prepare_shakespeare_char; prepare_shakespeare_char('data/shakespeare_char')"
To use your own data, create train.bin and val.bin with uint16 token IDs:
import numpy as np
import pickle
# Your tokenized data
train_ids = [...] # list of token IDs
val_ids = [...]
# Save binary files
np.array(train_ids, dtype=np.uint16).tofile('data/mydata/train.bin')
np.array(val_ids, dtype=np.uint16).tofile('data/mydata/val.bin')
# Save metadata
meta = {'vocab_size': YOUR_VOCAB_SIZE, 'stoi': {...}, 'itos': {...}}
with open('data/mydata/meta.pkl', 'wb') as f:
pickle.dump(meta, f)
Then train with: python train.py data.dir=data/mydata
| Metric | Description | Formula |
|---|---|---|
| Loss | Weighted cross-entropy on masked tokens | CE / p_mask |
| Perplexity (PPL) | Exponentiated loss (branching factor) | exp(loss) |
| Bits per Character (BPC) | Information-theoretic measure | loss / ln(2) |
Since MDMs use random masking, evaluation uses Monte Carlo sampling:
mc_samples different random maskings per batch# Standard evaluation
python eval.py checkpoint=out/ckpt.pt
# More accurate (more MC samples)
python eval.py checkpoint=out/ckpt.pt mc_samples=64 eval_iters=50
# Evaluate on train split
python eval.py checkpoint=out/ckpt.pt split=train
| Model | Params | Val Loss | Val PPL | Val BPC |
|---|---|---|---|---|
| 4L-4H-256D | ~2.5M | ~1.5 | ~4.5 | ~2.2 |
| 6L-6H-384D | ~10M | ~1.3 | ~3.7 | ~1.9 |
Results vary with training iterations and hyperparameters.
All scripts use Hydra for configuration management.
# Override any config parameter
python train.py training.max_iters=10000 model.n_layer=6
# Use experiment preset
python train.py +experiment=medium
# Enable wandb logging
python train.py wandb.enabled=true wandb.project=my-project
# Multiple overrides
python train.py model.n_layer=6 model.n_head=6 model.n_embd=384 \
training.learning_rate=6e-4 wandb.enabled=true
Organized wandb logging with metric groups:
# Enable wandb
python train.py wandb.enabled=true
# Custom project/run name
python train.py wandb.enabled=true wandb.project=mdm-experiments wandb.name=my-run
p_mask(t) = (1 - ε) * t + ε # ε=0.001
t=0: Almost no maskingt=1: Complete maskingL = E[CE(model(x_masked), x_original) / p_mask]
Importance weighting by 1/p_mask ensures proper likelihood estimation.
1 - s/t each stepStrategies: stochastic (random) or confidence (highest confidence first)
vocab_size (appended to vocabulary)# Default small model
python train.py
# Medium model with wandb
python train.py +experiment=medium wandb.enabled=true
# Custom model
python train.py model.n_layer=8 model.n_head=8 model.n_embd=512 \
training.max_iters=20000
# CPU training
python train.py system.device=cpu system.dtype=float32
# Basic sampling (use checkpoint path from training output)
python sample.py checkpoint=outputs/2024-01-01/12-00-00/checkpoints/ckpt.pt
# With prompt
python sample.py checkpoint=outputs/.../checkpoints/ckpt.pt prompt="To be or not"
# Adjust quality/diversity
python sample.py checkpoint=outputs/.../checkpoints/ckpt.pt \
sampling.steps=128 sampling.temperature=0.8
# Confidence-based sampling
python sample.py checkpoint=outputs/.../checkpoints/ckpt.pt sampling.strategy=confidence
# Multiple samples
python sample.py checkpoint=outputs/.../checkpoints/ckpt.pt num_samples=5
18 commits
Python
91.9%
Shell
8.1%
A clean, minimal implementation of Masked Diffusion Models for language modeling.
model.py # Bidirectional transformer (RoPE, RMSNorm, SwiGLU)
diffusion.py # Forward masking + reverse sampling
data.py # Data loading (nanoGPT-style binary format)
train.py # Training script (Hydra + wandb)
sample.py # Text generation
eval.py # Evaluation (loss, perplexity)
configs/
config.yaml # Default configuration
experiment/ # Experiment configs (small, medium)
# Install dependencies
pip install torch numpy hydra-core omegaconf wandb
# Train (auto-downloads Shakespeare)
python train.py
# Generate text (checkpoint saved in outputs/{date}/{time}/checkpoints/)
python sample.py checkpoint=outputs/2024-01-01/12-00-00/checkpoints/ckpt.pt prompt="ROMEO:"
# Evaluate
python eval.py checkpoint=outputs/2024-01-01/12-00-00/checkpoints/ckpt.pt
The default dataset is Tiny Shakespeare (~1MB, 1.1M characters), automatically downloaded on first run.
| Split | Characters | Tokens |
|---|---|---|
| Train | ~1M | ~1M |
| Val | ~100K | ~100K |
| Vocab | 65 unique characters | - |
Data format (nanoGPT-style):
train.bin, val.bin: Binary files of uint16 token IDsmeta.pkl: Vocabulary metadata (stoi, itos, vocab_size)Preparation (automatic or manual):
# Auto-downloads and prepares data on first train.py run
# Or manually:
python -c "from data import prepare_shakespeare_char; prepare_shakespeare_char('data/shakespeare_char')"
To use your own data, create train.bin and val.bin with uint16 token IDs:
import numpy as np
import pickle
# Your tokenized data
train_ids = [...] # list of token IDs
val_ids = [...]
# Save binary files
np.array(train_ids, dtype=np.uint16).tofile('data/mydata/train.bin')
np.array(val_ids, dtype=np.uint16).tofile('data/mydata/val.bin')
# Save metadata
meta = {'vocab_size': YOUR_VOCAB_SIZE, 'stoi': {...}, 'itos': {...}}
with open('data/mydata/meta.pkl', 'wb') as f:
pickle.dump(meta, f)
Then train with: python train.py data.dir=data/mydata
| Metric | Description | Formula |
|---|---|---|
| Loss | Weighted cross-entropy on masked tokens | CE / p_mask |
| Perplexity (PPL) | Exponentiated loss (branching factor) | exp(loss) |
| Bits per Character (BPC) | Information-theoretic measure | loss / ln(2) |
Since MDMs use random masking, evaluation uses Monte Carlo sampling:
mc_samples different random maskings per batch# Standard evaluation
python eval.py checkpoint=out/ckpt.pt
# More accurate (more MC samples)
python eval.py checkpoint=out/ckpt.pt mc_samples=64 eval_iters=50
# Evaluate on train split
python eval.py checkpoint=out/ckpt.pt split=train
| Model | Params | Val Loss | Val PPL | Val BPC |
|---|---|---|---|---|
| 4L-4H-256D | ~2.5M | ~1.5 | ~4.5 | ~2.2 |
| 6L-6H-384D | ~10M | ~1.3 | ~3.7 | ~1.9 |
Results vary with training iterations and hyperparameters.
All scripts use Hydra for configuration management.
# Override any config parameter
python train.py training.max_iters=10000 model.n_layer=6
# Use experiment preset
python train.py +experiment=medium
# Enable wandb logging
python train.py wandb.enabled=true wandb.project=my-project
# Multiple overrides
python train.py model.n_layer=6 model.n_head=6 model.n_embd=384 \
training.learning_rate=6e-4 wandb.enabled=true
Organized wandb logging with metric groups:
# Enable wandb
python train.py wandb.enabled=true
# Custom project/run name
python train.py wandb.enabled=true wandb.project=mdm-experiments wandb.name=my-run
p_mask(t) = (1 - ε) * t + ε # ε=0.001
t=0: Almost no maskingt=1: Complete maskingL = E[CE(model(x_masked), x_original) / p_mask]
Importance weighting by 1/p_mask ensures proper likelihood estimation.
1 - s/t each stepStrategies: stochastic (random) or confidence (highest confidence first)
vocab_size (appended to vocabulary)# Default small model
python train.py
# Medium model with wandb
python train.py +experiment=medium wandb.enabled=true
# Custom model
python train.py model.n_layer=8 model.n_head=8 model.n_embd=512 \
training.max_iters=20000
# CPU training
python train.py system.device=cpu system.dtype=float32
# Basic sampling (use checkpoint path from training output)
python sample.py checkpoint=outputs/2024-01-01/12-00-00/checkpoints/ckpt.pt
# With prompt
python sample.py checkpoint=outputs/.../checkpoints/ckpt.pt prompt="To be or not"
# Adjust quality/diversity
python sample.py checkpoint=outputs/.../checkpoints/ckpt.pt \
sampling.steps=128 sampling.temperature=0.8
# Confidence-based sampling
python sample.py checkpoint=outputs/.../checkpoints/ckpt.pt sampling.strategy=confidence
# Multiple samples
python sample.py checkpoint=outputs/.../checkpoints/ckpt.pt num_samples=5
18 commits
Python
91.9%
Shell
8.1%