File size: 11,221 Bytes
8b0aeb2 5255e5a 8b0aeb2 5255e5a 8b0aeb2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | # SAD: Soft Ancestor Diffusion: Optional Intermediate States for Masked Discrete Diffusion Language Models
SAD is a block-wise hierarchical diffusion model for unconditional text generation. It supports three variants:
- **SAD** (`sad`) β block-wise diffusion with learnable ancestor hierarchy
- **Block Diffusion** (`block_diffusion`) β block-wise binary mask diffusion (no ancestors)
- **AR** (`ar`) β standard autoregressive GPT-2 baseline
---
## Directory Structure
```
sad/
βββ configs/ # YAML config files
βββ scripts/ # Entry-point scripts (training / inference / eval)
βββ src/ # Core library
β βββ models/ # Model definitions
β βββ diffusion/ # Ancestor table & noisy state builder
β βββ losses/ # Loss functions
β βββ data/ # Data loaders
β βββ eval/ # Sampling & metric utilities
β βββ utils/ # Helpers
βββ data/ # Preprocessed data & caches (see below)
βββ models/ # Downloaded pretrained models (see below)
βββ tokenizers/ # Local GPT-2 tokenizer
βββ outputs/ # Training checkpoints & logs
βββ eval/ # Evaluation results
```
---
## What to Put in `data/` and `models/`
### `data/`
| File / Subdir | What it is | How to get it |
|---|---|---|
| `data/owt_cache/` | HuggingFace `openwebtext` cache | Auto-created on first training run |
| `data/hierarchy_prototypes_*.pt` | Hierarchy prototype tensors (k-means centroids) | Run `scripts/build_hierarchy.py learn` |
| `data/ancestor_lut_*.pt` | Sparse soft top-k ancestor lookup tables | Run `scripts/build_hierarchy.py build_lut` |
**You do NOT need to commit `data/` to git.** Add it to `.gitignore`.
### `models/`
| File / Subdir | What it is | How to get it |
|---|---|---|
| `models/gpt2/` | GPT-2 base weights (for leaf embeddings & eval LM) | `huggingface-cli download gpt2 --local-dir models/gpt2` |
| `models/gpt2-large/` | GPT-2 Large (for MAUVE featurization & gen-PPL eval) | `huggingface-cli download gpt2-large --local-dir models/gpt2-large` |
**You do NOT need to commit `models/` to git.** Add it to `.gitignore`.
### `tokenizers/`
| File / Subdir | What it is | How to get it |
|---|---|---|
| `tokenizers/gpt2/` | GPT-2 tokenizer files | `huggingface-cli download gpt2 --local-dir tokenizers/gpt2` |
---
## Scripts
### Training
| Script | Purpose | Quick Start |
|---|---|---|
| `scripts/train_sad.py` | Train SAD (with ancestors) | `torchrun --nproc_per_node=8 scripts/train_sad.py --config configs/sad_owt_b32_top3.yaml` |
| `scripts/train_block_diffusion.py` | Train block-mask diffusion (no ancestors) | `torchrun --nproc_per_node=8 scripts/train_block_diffusion.py --config configs/block_diffusion_owt_b32.yaml` |
| `scripts/train_ar.py` | Train AR baseline | `torchrun --nproc_per_node=8 scripts/train_ar.py --config configs/ar_owt.yaml` |
All training scripts support:
- `--config <path>` β YAML config (required)
- `--resume <path>` β resume from checkpoint
- `--num_steps <N>` β override training steps
- `--batch_size <N>` β override per-GPU batch size
### Inference / Sampling
| Script | Purpose | Quick Start |
|---|---|---|
| `scripts/inference_sad.py` | Sample from SAD checkpoint | `python scripts/inference_sad.py --config configs/sad_owt_b32_top3.yaml --checkpoint outputs/sad/latest.pt --num_samples 4` |
| `scripts/inference_block_diffusion.py` | Sample from block-diffusion checkpoint | `python scripts/inference_block_diffusion.py --config configs/block_diffusion_owt_b32.yaml --checkpoint outputs/block_diffusion/latest.pt --num_samples 4` |
| `scripts/inference_ar.py` | Sample from AR checkpoint | `python scripts/inference_ar.py --config configs/ar_owt.yaml --checkpoint outputs/ar_baseline/latest.pt --num_samples 4` |
### Evaluation
| Script | Purpose | Quick Start |
|---|---|---|
| `scripts/eval_gen_ppl.py` | Generative perplexity of SAD / block-diffusion samples under GPT-2 Large | `python scripts/eval_gen_ppl.py --checkpoint outputs/sad/latest.pt --model_type sad --num_samples 256` |
| `scripts/eval_ar_gen_ppl.py` | Generative perplexity of AR samples under GPT-2 Large | `python scripts/eval_ar_gen_ppl.py --checkpoint outputs/ar_baseline/latest.pt --num_samples 256` |
| `scripts/compute_mauve.py` | Compute MAUVE score against OpenWebText | `python scripts/compute_mauve.py --checkpoint outputs/sad/latest.pt --model_type sad --num_samples 5000` |
| `scripts/compute_diversity.py` | Compute distinct-n / repetition-n diversity metrics | `python scripts/compute_diversity.py --input samples.json --output diversity.json` |
### Hierarchy Construction (SAD only)
| Script | Purpose | Quick Start |
|---|---|---|
| `scripts/build_hierarchy.py learn` | Learn hierarchy prototypes via cosine k-means++ | `python scripts/build_hierarchy.py learn --config configs/sad_owt_b32_top3.yaml` |
| `scripts/build_hierarchy.py build_lut` | Build sparse soft top-k ancestor LUT | `python scripts/build_hierarchy.py build_lut --config configs/sad_owt_b32_top3.yaml` |
| `scripts/build_hierarchy.py extend` | Extend existing prototypes with deeper levels | `python scripts/build_hierarchy.py extend --input data/hierarchy_50257-128.pt --levels 128,32,8 --output data/hierarchy_50257-128-32-8.pt` |
| `scripts/build_hierarchy.py merge` | Merge per-level LUT files into one | `python scripts/build_hierarchy.py merge --source 1:data/lut_top3.pt --source 2:data/lut_top2.pt --output data/lut_mixed.pt` |
---
## Configs
| Config | Model | Description |
|---|---|---|
| `configs/sad_owt_b32_top3.yaml` | SAD | Base SAD on OpenWebText (512 tokens, 2 levels) |
| `configs/sad_owt_b32_h2_mixed.yaml` | SAD | batch_size=32, 2 ancestor levels, mixed top-k |
| `configs/sad_owt_b32_h3_mixed.yaml` | SAD | batch_size=32, 3 ancestor levels, mixed top-k |
| `configs/sad_owt_b32_top1.yaml` | SAD | top-1 ancestors |
| `configs/sad_owt_b32_top2.yaml` | SAD | top-2 ancestors |
| `configs/sad_owt_b32_top3.yaml` | SAD | top-3 ancestors |
| `configs/block_diffusion_owt_b32.yaml` | Block Diffusion | Mask-only block diffusion (no ancestors) |
| `configs/ar_owt.yaml` | AR | Standard autoregressive GPT-2 baseline |
---
## Core Source Modules
| File | Role |
|---|---|
| `src/models/sad_model.py` | `SADModel` β backbone with block-diff attention mask & FlexAttention |
| `src/models/ar_model.py` | `ARModel` β standard causal decoder baseline |
| `src/models/dit_components.py` | DiT blocks, Rotary embeddings, EmbeddingLayer |
| `src/diffusion/ancestor_table.py` | `AncestorTable` β fixed LUT + learnable ancestor embeddings |
| `src/diffusion/noisy_state.py` | `NoisyStateBuilder` β sample level per position & build noisy embeddings |
| `src/losses/sad_loss.py` | Leaf CE (ancestor CE loss was not experimented) |
| `src/data/__init__.py` | `build_owt_dataloader`, `build_debug_dataloader` |
| `src/eval/metrics.py` | Evaluation metric helpers |
---
# 1. SAD (`scripts/train_sad.py`)
## Quick Start
```bash
# Single GPU
python scripts/train_sad.py --config configs/sad_owt_b32_top3.yaml
# Multi-GPU DDP (e.g. 8 GPUs)
torchrun --nproc_per_node=8 scripts/train_sad.py \
--config configs/sad_owt_b32_top3.yaml
# Resume
torchrun --nproc_per_node=8 scripts/train_sad.py \
--config configs/sad_owt_b32_top3.yaml \
--resume outputs/sad/latest.pt
# Override steps / batch size
torchrun --nproc_per_node=8 scripts/train_sad.py \
--config configs/sad_owt_b32_top3.yaml \
--num_steps 500000 --batch_size 64
```
## Training Paradigm
- Sequence is divided into `block_size` token blocks; **intra-block bidirectional**, **inter-block causal**.
- Vectorized training: `x_full = [noisy | clean]`, single forward with block-diff mask:
| qβk | Allowed Condition |
|---------------|-------------------|
| noisy β noisy | Same block only |
| noisy β clean | Strictly earlier clean block |
| clean β clean | Same or earlier clean block |
| clean β noisy | Never |
- No timestep conditioning; only a single learnable `cond_bias`.
- Input is continuous embeddings (leaf / ancestor / mask), not discrete token ids.
- Loss computed only on the noisy half: leaf cross-entropy. (Note: `lambda_ancestor > 0` was not experimented.)
## Positional Encoding
Three-way additive + RoPE:
- `block_idx_embed` β cross-block position (AR)
- `intra_pos_embed` β intra-block position
- `segment_embed` β noisy(0) / clean(1)
- RoPE applied to both halves with `0..L-1`; noisy[i] and clean[i] have relative offset 0
## Config Skeleton (`configs/sad_owt_b32_top3.yaml`)
```yaml
model:
vocab_size: 50257
hidden_size: 768
n_blocks: 12
n_heads: 12
max_seq_len: 512
block_size: 8
num_levels: 2
level_sizes: [50257, 128]
ancestor:
lut_path: data/ancestor_lut_50257-128_top3_t1.0.pt # fallback: random debug LUT
proto_path: data/hierarchy_prototypes_50257-128.pt
loss:
lambda_ancestor: 0.0 # ancestor CE loss was not experimented
mask_only: false
training:
seed: 0
batch_size: 64 # per-GPU
num_steps: 1_000_000
lr: 3.0e-4
weight_decay: 0.01
grad_clip: 1.0
dtype: bf16
log_interval: 100
eval_interval: 5000
save_interval: 10000
compile: default # "off" to disable torch.compile
data:
dataset: openwebtext # "debug" uses TinyDebugDataset
seq_len: 512
cache_dir: data/owt_cache
mode: subsample # subsample | pack
logging:
use_wandb: true
project: sad
save_dir: outputs/sad
```
## DDP Invariants (do not change without care)
1. **AncestorTable is outside DDP**, but its parameters are in the optimizer. After `loss.backward()` and before clip, manually `dist.all_reduce(grad, op=AVG)` to sync its gradients.
2. **AncestorTable initial value broadcast**: after `build_ancestor_table`, broadcast parameters and buffers from rank 0 via `dist.broadcast` to all ranks, to counteract the drift from `set_seed(seed + local_rank)`.
3. **Streaming data sharding**: `build_owt_dataloader(shard_across_ranks=True)` for train, `False` for val (val only runs on rank 0; sharding would bias metrics to a single shard).
4. **FlexAttention ignores padding mask**, protected by an assert that `pad_token_id == eos_token_id`. Future multi-doc packing would need doc ids in `mask_mod`.
5. **Debug LUT cross-rank consistency**: `build_ancestor_table` uses an independent `torch.Generator` for random LUT, not the global RNG.
## Output
- `outputs/sad/ckpt_{step}.pt` β periodic checkpoint
- `outputs/sad/latest.pt` β latest checkpoint (overwritten)
- `outputs/sad/config.yaml` β runtime config snapshot
- wandb: `project=sad` (when `logging.use_wandb: true`)
Checkpoint contains `step` / `model` / `ancestor_table` / `optimizer` / `config` / `metrics`; resume restores all of them (data iterator position is not restored; streaming data restarts from the beginning).
## Evaluate
Runs on rank 0 only, every `eval_interval` steps, over `num_batches=50` batches (see `evaluate()`). Non-main ranks wait for rank 0 at the next backward all-reduce, so long eval will drop throughput but will not deadlock.
|