Automated push from Zaratan
Browse files- README.md +18 -0
- results/g1-agree-mdlm-s0.json +41 -0
- results/g1-agree-mdlm-s1.json +41 -0
- results/g1-agree-selfcorrupt-s0.json +41 -0
- results/g1-agree-selfcorrupt-s1.json +41 -0
- results/g1-copy-mdlm-s0.json +41 -0
- results/g1-copy-mdlm-s1.json +41 -0
- results/g1-copy-selfcorrupt-s0.json +41 -0
- results/g1-copy-selfcorrupt-s1.json +41 -0
- results/g1-parity-mdlm-s0.json +41 -0
- results/g1-parity-mdlm-s1.json +41 -0
- results/g1-parity-selfcorrupt-s0.json +41 -0
- results/g1-parity-selfcorrupt-s1.json +41 -0
- slurm/finalize.sh +103 -0
- slurm/grid1.sh +36 -0
- slurm/watchdog.sh +42 -0
- src/data.py +132 -0
- src/diffusion.py +159 -0
- src/model.py +135 -0
- src/train.py +171 -0
README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
tags: [masked-diffusion, discrete-diffusion, length-generalization, positional-encoding, from-scratch]
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Length Generalization in Masked Diffusion Language Models
|
| 7 |
+
|
| 8 |
+
From-scratch masked diffusion language models (MDLM-style absorbing state) and
|
| 9 |
+
matched autoregressive baselines, trained on arithmetic and algorithmic tasks and
|
| 10 |
+
evaluated far beyond the lengths seen in training.
|
| 11 |
+
|
| 12 |
+
**No pretrained weights or tokenizers are used anywhere.**
|
| 13 |
+
|
| 14 |
+
Code: https://github.com/GIND123/MSML612
|
| 15 |
+
|
| 16 |
+
## Results
|
| 17 |
+
|
| 18 |
+
_(pending)_
|
results/g1-agree-mdlm-s0.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 0.0,
|
| 4 |
+
"2": 0.0,
|
| 5 |
+
"4": 0.0,
|
| 6 |
+
"8": 0.0,
|
| 7 |
+
"16": 0.0,
|
| 8 |
+
"32": 0.0,
|
| 9 |
+
"64": 0.0,
|
| 10 |
+
"128": 1.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "mdlm",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "agree",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 0,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-agree-mdlm-s0"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 3.730570578575134,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 1
|
| 40 |
+
}
|
| 41 |
+
}
|
results/g1-agree-mdlm-s1.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 0.0,
|
| 4 |
+
"2": 0.0,
|
| 5 |
+
"4": 0.0,
|
| 6 |
+
"8": 0.0,
|
| 7 |
+
"16": 0.0,
|
| 8 |
+
"32": 0.0,
|
| 9 |
+
"64": 0.0,
|
| 10 |
+
"128": 1.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "mdlm",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "agree",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 1,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-agree-mdlm-s1"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 3.6946280360221864,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 1
|
| 40 |
+
}
|
| 41 |
+
}
|
results/g1-agree-selfcorrupt-s0.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 0.0,
|
| 4 |
+
"2": 0.0,
|
| 5 |
+
"4": 0.0,
|
| 6 |
+
"8": 0.0,
|
| 7 |
+
"16": 0.0,
|
| 8 |
+
"32": 0.0,
|
| 9 |
+
"64": 0.0,
|
| 10 |
+
"128": 0.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "selfcorrupt",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "agree",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 0,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-agree-selfcorrupt-s0"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 4.3024773756663,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 2
|
| 40 |
+
}
|
| 41 |
+
}
|
results/g1-agree-selfcorrupt-s1.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 0.0,
|
| 4 |
+
"2": 0.0,
|
| 5 |
+
"4": 0.0,
|
| 6 |
+
"8": 0.0,
|
| 7 |
+
"16": 0.0,
|
| 8 |
+
"32": 0.0,
|
| 9 |
+
"64": 0.0,
|
| 10 |
+
"128": 0.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "selfcorrupt",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "agree",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 1,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-agree-selfcorrupt-s1"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 4.266684722900391,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 2
|
| 40 |
+
}
|
| 41 |
+
}
|
results/g1-copy-mdlm-s0.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 1.0,
|
| 4 |
+
"2": 1.0,
|
| 5 |
+
"4": 1.0,
|
| 6 |
+
"8": 1.0,
|
| 7 |
+
"16": 1.0,
|
| 8 |
+
"32": 1.0,
|
| 9 |
+
"64": 1.0,
|
| 10 |
+
"128": 1.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "mdlm",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "copy",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 0,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-copy-mdlm-s0"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 3.757214069366455,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 1
|
| 40 |
+
}
|
| 41 |
+
}
|
results/g1-copy-mdlm-s1.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 1.0,
|
| 4 |
+
"2": 1.0,
|
| 5 |
+
"4": 1.0,
|
| 6 |
+
"8": 1.0,
|
| 7 |
+
"16": 1.0,
|
| 8 |
+
"32": 1.0,
|
| 9 |
+
"64": 1.0,
|
| 10 |
+
"128": 1.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "mdlm",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "copy",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 1,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-copy-mdlm-s1"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 3.7269402066866557,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 1
|
| 40 |
+
}
|
| 41 |
+
}
|
results/g1-copy-selfcorrupt-s0.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 0.0,
|
| 4 |
+
"2": 0.0,
|
| 5 |
+
"4": 0.0,
|
| 6 |
+
"8": 0.0,
|
| 7 |
+
"16": 0.0,
|
| 8 |
+
"32": 0.0,
|
| 9 |
+
"64": 0.0,
|
| 10 |
+
"128": 0.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "selfcorrupt",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "copy",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 0,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-copy-selfcorrupt-s0"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 4.29505068063736,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 2
|
| 40 |
+
}
|
| 41 |
+
}
|
results/g1-copy-selfcorrupt-s1.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 0.0,
|
| 4 |
+
"2": 0.0,
|
| 5 |
+
"4": 0.0,
|
| 6 |
+
"8": 0.0,
|
| 7 |
+
"16": 0.0,
|
| 8 |
+
"32": 0.0,
|
| 9 |
+
"64": 0.0,
|
| 10 |
+
"128": 0.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "selfcorrupt",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "copy",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 1,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-copy-selfcorrupt-s1"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 4.269979015986125,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 2
|
| 40 |
+
}
|
| 41 |
+
}
|
results/g1-parity-mdlm-s0.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 0.0,
|
| 4 |
+
"2": 0.0,
|
| 5 |
+
"4": 0.0,
|
| 6 |
+
"8": 0.0,
|
| 7 |
+
"16": 0.0,
|
| 8 |
+
"32": 0.0,
|
| 9 |
+
"64": 0.0,
|
| 10 |
+
"128": 1.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "mdlm",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "parity",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 0,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-parity-mdlm-s0"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 3.7436817288398743,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 1
|
| 40 |
+
}
|
| 41 |
+
}
|
results/g1-parity-mdlm-s1.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 0.0,
|
| 4 |
+
"2": 0.0,
|
| 5 |
+
"4": 0.0,
|
| 6 |
+
"8": 0.0,
|
| 7 |
+
"16": 0.0,
|
| 8 |
+
"32": 0.0,
|
| 9 |
+
"64": 0.0,
|
| 10 |
+
"128": 1.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "mdlm",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "parity",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 1,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-parity-mdlm-s1"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 3.6899612665176393,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 1
|
| 40 |
+
}
|
| 41 |
+
}
|
results/g1-parity-selfcorrupt-s0.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 0.0,
|
| 4 |
+
"2": 0.0,
|
| 5 |
+
"4": 0.0,
|
| 6 |
+
"8": 0.0,
|
| 7 |
+
"16": 0.0,
|
| 8 |
+
"32": 0.0,
|
| 9 |
+
"64": 0.0,
|
| 10 |
+
"128": 0.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "selfcorrupt",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "parity",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 0,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-parity-selfcorrupt-s0"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 4.269179002443949,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 2
|
| 40 |
+
}
|
| 41 |
+
}
|
results/g1-parity-selfcorrupt-s1.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"accuracy_by_passes": {
|
| 3 |
+
"1": 0.0,
|
| 4 |
+
"2": 0.0,
|
| 5 |
+
"4": 0.0,
|
| 6 |
+
"8": 0.0,
|
| 7 |
+
"16": 0.0,
|
| 8 |
+
"32": 0.0,
|
| 9 |
+
"64": 0.0,
|
| 10 |
+
"128": 0.0
|
| 11 |
+
},
|
| 12 |
+
"args": {
|
| 13 |
+
"objective": "selfcorrupt",
|
| 14 |
+
"k_frac": 0.25,
|
| 15 |
+
"correct_weight": 1.0,
|
| 16 |
+
"task": "parity",
|
| 17 |
+
"coupling": 4,
|
| 18 |
+
"seq_len": 128,
|
| 19 |
+
"n_train": 200000,
|
| 20 |
+
"n_eval": 500,
|
| 21 |
+
"d": 384,
|
| 22 |
+
"layers": 6,
|
| 23 |
+
"heads": 6,
|
| 24 |
+
"pe": "rope",
|
| 25 |
+
"bs": 64,
|
| 26 |
+
"steps": 20000,
|
| 27 |
+
"lr": 0.0003,
|
| 28 |
+
"warmup": 500,
|
| 29 |
+
"seed": 1,
|
| 30 |
+
"data_root": "data",
|
| 31 |
+
"eval_bs": 100,
|
| 32 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/g1-parity-selfcorrupt-s1"
|
| 33 |
+
},
|
| 34 |
+
"minutes": 4.223804748058319,
|
| 35 |
+
"compute": {
|
| 36 |
+
"params": 10660608,
|
| 37 |
+
"train_tokens": 163840000,
|
| 38 |
+
"train_flops_approx": 10479804088320000,
|
| 39 |
+
"fwd_per_step": 2
|
| 40 |
+
}
|
| 41 |
+
}
|
slurm/finalize.sh
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Runs on the cluster after a grid finishes (SLURM dependency), so results are
|
| 3 |
+
# collected, backed up and published even if the laptop is closed, the network
|
| 4 |
+
# drops, or the session that launched the work is long gone.
|
| 5 |
+
#
|
| 6 |
+
# 1. regenerate figures + tables from every run
|
| 7 |
+
# 2. back up to HOME (the only nightly-backed-up tier)
|
| 8 |
+
# 3. back up weights to SHELL (1 TB, no 90-day purge unlike scratch)
|
| 9 |
+
# 4. push to the Hugging Face Hub, with retries
|
| 10 |
+
#
|
| 11 |
+
#SBATCH -J finalize
|
| 12 |
+
#SBATCH -t 03:00:00
|
| 13 |
+
#SBATCH -c 4
|
| 14 |
+
#SBATCH --mem=16g
|
| 15 |
+
#SBATCH -p standard
|
| 16 |
+
#SBATCH -o logs/finalize-%j.out
|
| 17 |
+
#SBATCH --open-mode=append
|
| 18 |
+
|
| 19 |
+
source /etc/profile
|
| 20 |
+
module load pytorch/2.0.1
|
| 21 |
+
|
| 22 |
+
USER_SCR=/scratch/zt1/project/msml612/user/$USER
|
| 23 |
+
STAR=$USER_SCR/pcmd
|
| 24 |
+
export PYTHONPATH=$USER_SCR/pylibs:$PYTHONPATH
|
| 25 |
+
# Compute nodes cannot reach HF's Xet CAS endpoint; force the plain LFS path.
|
| 26 |
+
export HF_HUB_DISABLE_XET=1
|
| 27 |
+
|
| 28 |
+
HOME_BK=$HOME/pcmd-backup
|
| 29 |
+
SHELL_BK=/afs/shell.umd.edu/project/msml612/user-$USER/msml612-weights
|
| 30 |
+
|
| 31 |
+
echo "=== 1. collect + plot ==="
|
| 32 |
+
cd $STAR/src
|
| 33 |
+
RUNS=$STAR/runs OUT=$STAR/figures python collect.py || echo "collect failed (continuing)"
|
| 34 |
+
|
| 35 |
+
echo "=== 2. back up to HOME (backed up nightly) ==="
|
| 36 |
+
mkdir -p $HOME_BK/{results,figures,src,slurm}
|
| 37 |
+
cp -f $STAR/figures/* $HOME_BK/figures/ 2>/dev/null
|
| 38 |
+
cp -f $STAR/src/*.py $HOME_BK/src/ 2>/dev/null
|
| 39 |
+
cp -f $STAR/*.sh $HOME_BK/slurm/ 2>/dev/null
|
| 40 |
+
for d in $STAR/runs/*/; do
|
| 41 |
+
n=$(basename $d)
|
| 42 |
+
[ -f "$d/result.json" ] && cp -f "$d/result.json" "$HOME_BK/results/$n.json"
|
| 43 |
+
done
|
| 44 |
+
echo " results backed up: $(ls $HOME_BK/results | wc -l)"
|
| 45 |
+
|
| 46 |
+
echo "=== 3. back up weights to SHELL (not purged) ==="
|
| 47 |
+
if mkdir -p $SHELL_BK 2>/dev/null; then
|
| 48 |
+
for d in $STAR/runs/*/; do
|
| 49 |
+
n=$(basename $d)
|
| 50 |
+
[ -f "$d/model.pt" ] && cp -f "$d/model.pt" "$SHELL_BK/$n.pt" 2>/dev/null
|
| 51 |
+
done
|
| 52 |
+
echo " weights backed up: $(ls $SHELL_BK 2>/dev/null | wc -l)"
|
| 53 |
+
else
|
| 54 |
+
echo " SHELL not writable from this node - weights remain on scratch"
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
echo "=== 4. push to Hugging Face (with retries) ==="
|
| 58 |
+
# A transient network failure must not mean the results never reach the Hub, so
|
| 59 |
+
# retry with backoff rather than giving up on the first error.
|
| 60 |
+
for attempt in 1 2 3 4 5; do
|
| 61 |
+
python - <<'PY' && break
|
| 62 |
+
import os, pathlib, sys
|
| 63 |
+
from huggingface_hub import HfApi
|
| 64 |
+
|
| 65 |
+
tok = pathlib.Path(os.path.expanduser("~/.hf_token")).read_text().strip()
|
| 66 |
+
star = os.path.expanduser(f"/scratch/zt1/project/msml612/user/{os.environ['USER']}/pcmd")
|
| 67 |
+
stage = pathlib.Path(os.path.expanduser("~/pcmd-backup"))
|
| 68 |
+
repo = "GOVINDFROM/parallel-consistent-masked-diffusion"
|
| 69 |
+
|
| 70 |
+
summary = pathlib.Path(star, "figures", "summary.md")
|
| 71 |
+
table = summary.read_text() if summary.exists() else "_(pending)_"
|
| 72 |
+
|
| 73 |
+
(stage / "README.md").write_text(f"""---
|
| 74 |
+
license: apache-2.0
|
| 75 |
+
tags: [masked-diffusion, discrete-diffusion, length-generalization, positional-encoding, from-scratch]
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
# Length Generalization in Masked Diffusion Language Models
|
| 79 |
+
|
| 80 |
+
From-scratch masked diffusion language models (MDLM-style absorbing state) and
|
| 81 |
+
matched autoregressive baselines, trained on arithmetic and algorithmic tasks and
|
| 82 |
+
evaluated far beyond the lengths seen in training.
|
| 83 |
+
|
| 84 |
+
**No pretrained weights or tokenizers are used anywhere.**
|
| 85 |
+
|
| 86 |
+
Code: https://github.com/GIND123/MSML612
|
| 87 |
+
|
| 88 |
+
## Results
|
| 89 |
+
|
| 90 |
+
{table}
|
| 91 |
+
""")
|
| 92 |
+
|
| 93 |
+
api = HfApi(token=tok)
|
| 94 |
+
api.create_repo(repo, repo_type="model", private=True, exist_ok=True)
|
| 95 |
+
api.upload_folder(folder_path=str(stage), repo_id=repo, repo_type="model",
|
| 96 |
+
commit_message="Automated push from Zaratan")
|
| 97 |
+
print("pushed:", f"https://huggingface.co/{repo}")
|
| 98 |
+
PY
|
| 99 |
+
echo " push attempt $attempt failed; sleeping $((attempt * 60))s"
|
| 100 |
+
sleep $((attempt * 60))
|
| 101 |
+
done
|
| 102 |
+
|
| 103 |
+
echo "=== finalize complete ==="
|
slurm/grid1.sh
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Grid 1 — learnability gate and the first quality-vs-parallelism curves.
|
| 3 |
+
#
|
| 4 |
+
# Doubles as a gate: if a model cannot solve a probe even at FULLY SEQUENTIAL
|
| 5 |
+
# decoding (128 passes for a 128-token canvas), the task is not learnable at
|
| 6 |
+
# this scale and no parallel-decoding claim built on it would mean anything.
|
| 7 |
+
# Launching a large grid before establishing this is the mistake that cost a
|
| 8 |
+
# day on the previous project.
|
| 9 |
+
#SBATCH -J g1
|
| 10 |
+
#SBATCH -p gpu-h100
|
| 11 |
+
#SBATCH --gpus=h100:1
|
| 12 |
+
#SBATCH -c 8
|
| 13 |
+
#SBATCH --mem=64g
|
| 14 |
+
#SBATCH -t 04:00:00
|
| 15 |
+
#SBATCH -o logs/g1-%A_%a.out
|
| 16 |
+
#SBATCH --array=0-11%12
|
| 17 |
+
|
| 18 |
+
source /etc/profile
|
| 19 |
+
SCR=/scratch/zt1/project/msml612/user/$USER
|
| 20 |
+
module load python/3.10.10
|
| 21 |
+
export PYTHONPATH=
|
| 22 |
+
PY=$SCR/hf_env/bin/python
|
| 23 |
+
cd $SCR/pcmd/src
|
| 24 |
+
|
| 25 |
+
I=$SLURM_ARRAY_TASK_ID
|
| 26 |
+
TASKS=(copy agree parity); OBJ=(mdlm selfcorrupt); SEEDS=(0 1)
|
| 27 |
+
T=${TASKS[$((I / 4))]}
|
| 28 |
+
O=${OBJ[$(((I % 4) / 2))]}
|
| 29 |
+
S=${SEEDS[$((I % 2))]}
|
| 30 |
+
|
| 31 |
+
NAME="g1-$T-$O-s$S"
|
| 32 |
+
echo "=== $NAME ==="
|
| 33 |
+
$PY train.py --task $T --objective $O --coupling 4 --seed $S \
|
| 34 |
+
--seq_len 128 --n_train 200000 --n_eval 500 --eval_bs 100 \
|
| 35 |
+
--d 384 --layers 6 --heads 6 --bs 64 --steps 20000 --lr 3e-4 \
|
| 36 |
+
--out $SCR/pcmd/runs/$NAME
|
slurm/watchdog.sh
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Safety net: re-runs finalize periodically, independent of any grid.
|
| 3 |
+
#
|
| 4 |
+
# finalize.sh is chained to each grid, but that only fires when a grid ends
|
| 5 |
+
# cleanly. If a grid is cancelled, times out, or the chained job itself fails,
|
| 6 |
+
# results would sit on scratch un-published. This resubmits itself every few
|
| 7 |
+
# hours so collection and the Hugging Face push keep happening regardless of
|
| 8 |
+
# what the laptop, the network, or the login session are doing.
|
| 9 |
+
#
|
| 10 |
+
# Start once with: sbatch watchdog.sh
|
| 11 |
+
# Stop with: scancel -n watchdog
|
| 12 |
+
#
|
| 13 |
+
#SBATCH -J watchdog
|
| 14 |
+
#SBATCH -t 00:45:00
|
| 15 |
+
#SBATCH -c 2
|
| 16 |
+
#SBATCH --mem=8g
|
| 17 |
+
#SBATCH -p standard
|
| 18 |
+
#SBATCH -o logs/watchdog-%j.out
|
| 19 |
+
|
| 20 |
+
SCR=/scratch/zt1/project/msml612/user/$USER
|
| 21 |
+
cd $SCR/pcmd
|
| 22 |
+
|
| 23 |
+
RUNS_NOW=$(ls $SCR/pcmd/runs 2>/dev/null | wc -l)
|
| 24 |
+
LAST=$(cat $SCR/pcmd/.watchdog_last 2>/dev/null || echo 0)
|
| 25 |
+
echo "$(date): runs=$RUNS_NOW last_published=$LAST"
|
| 26 |
+
|
| 27 |
+
# Only spend effort when something actually changed.
|
| 28 |
+
if [ "$RUNS_NOW" -ne "$LAST" ]; then
|
| 29 |
+
echo "new results detected - running finalize"
|
| 30 |
+
bash $SCR/pcmd/finalize.sh
|
| 31 |
+
echo "$RUNS_NOW" > $SCR/pcmd/.watchdog_last
|
| 32 |
+
else
|
| 33 |
+
echo "nothing new since last publish"
|
| 34 |
+
fi
|
| 35 |
+
|
| 36 |
+
# Re-arm unless someone asked it to stop.
|
| 37 |
+
if [ ! -f $SCR/pcmd/.watchdog_stop ]; then
|
| 38 |
+
sbatch --begin=now+3hours $SCR/pcmd/watchdog.sh
|
| 39 |
+
echo "re-armed for +3h"
|
| 40 |
+
else
|
| 41 |
+
echo "stop file present - not re-arming"
|
| 42 |
+
fi
|
src/data.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Datasets: text8 for language modelling, plus dependency-controlled probes.
|
| 2 |
+
|
| 3 |
+
text8 is the standard small-scale benchmark for discrete diffusion language
|
| 4 |
+
models (D3PM, SEDD, MDLM all report bits-per-character on it), which is what
|
| 5 |
+
makes a from-scratch comparison meaningful.
|
| 6 |
+
|
| 7 |
+
The probes exist because text8 alone cannot isolate the phenomenon we study.
|
| 8 |
+
Parallel decoding fails when simultaneously-revealed tokens depend on each
|
| 9 |
+
other, so we need tasks where that dependence is a knob rather than a property
|
| 10 |
+
of English.
|
| 11 |
+
"""
|
| 12 |
+
import os
|
| 13 |
+
import numpy as np
|
| 14 |
+
|
| 15 |
+
TEXT8_URL = "http://mattmahoney.net/dc/text8.zip"
|
| 16 |
+
# text8 is lowercase letters plus space: a 27-symbol alphabet, no tokenizer.
|
| 17 |
+
ALPHABET = " abcdefghijklmnopqrstuvwxyz"
|
| 18 |
+
SPECIALS = ["[MASK]", "[PAD]"]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class CharTokenizer:
|
| 22 |
+
def __init__(self):
|
| 23 |
+
self.itos = SPECIALS + list(ALPHABET)
|
| 24 |
+
self.stoi = {c: i for i, c in enumerate(self.itos)}
|
| 25 |
+
self.mask = self.stoi["[MASK]"]
|
| 26 |
+
self.pad = self.stoi["[PAD]"]
|
| 27 |
+
|
| 28 |
+
def __len__(self):
|
| 29 |
+
return len(self.itos)
|
| 30 |
+
|
| 31 |
+
def encode(self, s):
|
| 32 |
+
return [self.stoi[c] for c in s if c in self.stoi]
|
| 33 |
+
|
| 34 |
+
def decode(self, ids):
|
| 35 |
+
return "".join(self.itos[i] for i in ids)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def load_text8(root, seq_len=256):
|
| 39 |
+
"""Standard 90M/5M/5M character split used throughout the literature."""
|
| 40 |
+
raw = os.path.join(root, "text8")
|
| 41 |
+
if not os.path.exists(raw):
|
| 42 |
+
os.makedirs(root, exist_ok=True)
|
| 43 |
+
import zipfile, io, requests # only needed on first download
|
| 44 |
+
r = requests.get(TEXT8_URL, timeout=300)
|
| 45 |
+
r.raise_for_status()
|
| 46 |
+
with zipfile.ZipFile(io.BytesIO(r.content)) as z:
|
| 47 |
+
z.extract("text8", root)
|
| 48 |
+
text = open(raw).read()
|
| 49 |
+
assert len(text) == 100_000_000, f"unexpected text8 length {len(text)}"
|
| 50 |
+
|
| 51 |
+
tok = CharTokenizer()
|
| 52 |
+
cache = os.path.join(root, "text8.npy")
|
| 53 |
+
if os.path.exists(cache):
|
| 54 |
+
ids = np.load(cache)
|
| 55 |
+
else:
|
| 56 |
+
ids = np.array(tok.encode(text), dtype=np.uint8)
|
| 57 |
+
np.save(cache, ids)
|
| 58 |
+
|
| 59 |
+
tr, va = ids[:90_000_000], ids[90_000_000:95_000_000]
|
| 60 |
+
te = ids[95_000_000:]
|
| 61 |
+
|
| 62 |
+
def chunk(a):
|
| 63 |
+
n = len(a) // seq_len
|
| 64 |
+
return a[: n * seq_len].reshape(n, seq_len).astype(np.int64)
|
| 65 |
+
|
| 66 |
+
return chunk(tr), chunk(va), chunk(te), tok
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ---------------------------------------------------------------------------
|
| 70 |
+
# Dependency-controlled probes.
|
| 71 |
+
#
|
| 72 |
+
# Each item is a sequence whose tokens must agree with each other. `coupling`
|
| 73 |
+
# sets how many positions are bound together, which is the exact quantity that
|
| 74 |
+
# breaks parallel decoding: revealing two mutually-dependent positions in one
|
| 75 |
+
# step forces the model to guess them independently.
|
| 76 |
+
# ---------------------------------------------------------------------------
|
| 77 |
+
|
| 78 |
+
def build_probe(n, seq_len, coupling, seed=0, task="copy"):
|
| 79 |
+
"""
|
| 80 |
+
copy : second half must equal the first half. Each answer position is
|
| 81 |
+
fixed by one context position, so answer positions are mutually
|
| 82 |
+
INDEPENDENT given the context - parallel decoding should be safe.
|
| 83 |
+
agree : within each group of `coupling` positions all tokens must be equal
|
| 84 |
+
AND adjacent groups must differ. The value of a group is free, so
|
| 85 |
+
its members are mutually DEPENDENT: revealing two of them in one
|
| 86 |
+
step is a 1/V coin flip. This is the failure mode, isolated.
|
| 87 |
+
parity : groups are a run of bits whose last element is the XOR of the
|
| 88 |
+
rest, with adjacent groups forced to differ, so the group is
|
| 89 |
+
jointly constrained without any member being determined alone.
|
| 90 |
+
|
| 91 |
+
The adjacent-groups-differ rule matters: without it a model that emits one
|
| 92 |
+
constant token everywhere satisfies "all members agree" perfectly, and an
|
| 93 |
+
untrained model scores 100% at maximum parallelism. That degenerate solution
|
| 94 |
+
made the first version of this metric meaningless.
|
| 95 |
+
"""
|
| 96 |
+
tok = CharTokenizer()
|
| 97 |
+
rng = np.random.default_rng(seed)
|
| 98 |
+
V = 2 + 26 # specials + letters
|
| 99 |
+
lo, hi = 2, V # sample letters only
|
| 100 |
+
|
| 101 |
+
X = np.zeros((n, seq_len), dtype=np.int64)
|
| 102 |
+
for i in range(n):
|
| 103 |
+
if task == "copy":
|
| 104 |
+
half = seq_len // 2
|
| 105 |
+
src = rng.integers(lo, hi, size=half)
|
| 106 |
+
X[i, :half] = src
|
| 107 |
+
X[i, half:half * 2] = src
|
| 108 |
+
elif task == "agree":
|
| 109 |
+
g, prev = coupling, -1
|
| 110 |
+
for s in range(0, seq_len, g):
|
| 111 |
+
v = int(rng.integers(lo, hi))
|
| 112 |
+
while v == prev: # adjacent groups must differ
|
| 113 |
+
v = int(rng.integers(lo, hi))
|
| 114 |
+
X[i, s:s + g] = v
|
| 115 |
+
prev = v
|
| 116 |
+
elif task == "parity":
|
| 117 |
+
g, prev = coupling, None
|
| 118 |
+
for s in range(0, seq_len, g):
|
| 119 |
+
end = min(s + g, seq_len)
|
| 120 |
+
if end - s < 2:
|
| 121 |
+
X[i, s:end] = lo
|
| 122 |
+
continue
|
| 123 |
+
while True:
|
| 124 |
+
bits = list(rng.integers(0, 2, size=end - s - 1))
|
| 125 |
+
vals = bits + [int(sum(bits) % 2)]
|
| 126 |
+
if vals != prev: # adjacent groups must differ
|
| 127 |
+
break
|
| 128 |
+
X[i, s:end] = [lo + v for v in vals]
|
| 129 |
+
prev = vals
|
| 130 |
+
else:
|
| 131 |
+
raise ValueError(task)
|
| 132 |
+
return X, tok
|
src/diffusion.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Masked diffusion: training objectives and decoding strategies.
|
| 2 |
+
|
| 3 |
+
The central problem this file exists to study:
|
| 4 |
+
|
| 5 |
+
MDLM training teaches each masked position its MARGINAL p(x_i | observed).
|
| 6 |
+
Parallel decoding reveals k positions at once by sampling the PRODUCT of
|
| 7 |
+
those marginals. The truth is the joint, and the gap is exactly the mutual
|
| 8 |
+
information among the revealed positions. Quality therefore falls as the
|
| 9 |
+
parallelism rises - which is the only reason to use diffusion in the first
|
| 10 |
+
place.
|
| 11 |
+
|
| 12 |
+
Every published fix reorders or gates commits at INFERENCE. The model has still
|
| 13 |
+
never seen a partially-decoded state containing its own correlated mistakes.
|
| 14 |
+
`self_corrupted_loss` changes that at training time.
|
| 15 |
+
"""
|
| 16 |
+
import torch
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# --------------------------------------------------------------- training ---
|
| 21 |
+
def mdlm_loss(model, x0, tok, t_min=None, fixed_t=None):
|
| 22 |
+
"""Standard MDLM objective: mask each token independently with prob t,
|
| 23 |
+
predict the originals, weight by 1/t."""
|
| 24 |
+
B, L = x0.shape
|
| 25 |
+
dev = x0.device
|
| 26 |
+
t_min = t_min if t_min is not None else 1.0 / L
|
| 27 |
+
t = (torch.full((B,), fixed_t, device=dev) if fixed_t
|
| 28 |
+
else t_min + (1.0 - t_min) * torch.rand(B, device=dev))
|
| 29 |
+
m = torch.rand(B, L, device=dev) < t[:, None]
|
| 30 |
+
m[torch.arange(B, device=dev), torch.randint(0, L, (B,), device=dev)] = True
|
| 31 |
+
|
| 32 |
+
xt = torch.where(m, torch.full_like(x0, tok.mask), x0)
|
| 33 |
+
logits = model(xt)
|
| 34 |
+
ce = F.cross_entropy(logits.reshape(-1, logits.size(-1)), x0.reshape(-1),
|
| 35 |
+
reduction="none").view(B, L)
|
| 36 |
+
return ((1.0 / t) * (ce * m).sum(1) / L).mean()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def self_corrupted_loss(model, x0, tok, k_frac=0.25, t_min=None,
|
| 40 |
+
correct_weight=1.0):
|
| 41 |
+
"""OUR METHOD. Train on states that parallel decoding actually produces.
|
| 42 |
+
|
| 43 |
+
1. mask as usual and take a NO-GRAD pass to get the current marginals
|
| 44 |
+
2. reveal a random subset of the masked positions *in parallel*, sampling
|
| 45 |
+
each independently - i.e. commit exactly the kind of jointly-inconsistent
|
| 46 |
+
tokens that parallel decoding commits
|
| 47 |
+
3. train on the remaining masked positions given that self-generated
|
| 48 |
+
context, so the model learns p(x_i | observed, its own parallel samples)
|
| 49 |
+
4. additionally train it to REPAIR the committed tokens, which turns a
|
| 50 |
+
wrong commit from an absorbing error into a recoverable one
|
| 51 |
+
|
| 52 |
+
The model is therefore optimised for the distribution it meets at inference
|
| 53 |
+
rather than for ground-truth context it will never see.
|
| 54 |
+
"""
|
| 55 |
+
B, L = x0.shape
|
| 56 |
+
dev = x0.device
|
| 57 |
+
t_min = t_min if t_min is not None else 1.0 / L
|
| 58 |
+
t = t_min + (1.0 - t_min) * torch.rand(B, device=dev)
|
| 59 |
+
m = torch.rand(B, L, device=dev) < t[:, None]
|
| 60 |
+
m[torch.arange(B, device=dev), torch.randint(0, L, (B,), device=dev)] = True
|
| 61 |
+
xt = torch.where(m, torch.full_like(x0, tok.mask), x0)
|
| 62 |
+
|
| 63 |
+
with torch.no_grad():
|
| 64 |
+
probs = model(xt).softmax(-1)
|
| 65 |
+
samp = torch.distributions.Categorical(probs=probs).sample()
|
| 66 |
+
|
| 67 |
+
# commit a random subset of masked positions from the model's own samples
|
| 68 |
+
commit = m & (torch.rand(B, L, device=dev) < k_frac)
|
| 69 |
+
x_self = torch.where(commit, samp, xt)
|
| 70 |
+
still = m & ~commit
|
| 71 |
+
|
| 72 |
+
logits = model(x_self)
|
| 73 |
+
ce = F.cross_entropy(logits.reshape(-1, logits.size(-1)), x0.reshape(-1),
|
| 74 |
+
reduction="none").view(B, L)
|
| 75 |
+
|
| 76 |
+
denom = still.sum(1).clamp_min(1)
|
| 77 |
+
main = ((1.0 / t) * (ce * still).sum(1) / denom).mean()
|
| 78 |
+
# repair term: committed positions are supervised against ground truth too
|
| 79 |
+
rep = (ce * commit).sum(1) / commit.sum(1).clamp_min(1)
|
| 80 |
+
return main + correct_weight * rep.mean()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# --------------------------------------------------------------- decoding ---
|
| 84 |
+
@torch.no_grad()
|
| 85 |
+
def decode(model, shape, tok, steps, device, strategy="confidence",
|
| 86 |
+
temperature=0.0, entropy_tau=None, x_init=None, keep_trace=False):
|
| 87 |
+
"""Reveal the canvas over `steps` passes. Fewer passes = more tokens
|
| 88 |
+
committed per pass = more parallelism = more conditional-independence error.
|
| 89 |
+
|
| 90 |
+
strategies
|
| 91 |
+
confidence : reveal the most confident masked positions (the standard)
|
| 92 |
+
random : reveal a random subset (ablation: is confidence doing work?)
|
| 93 |
+
entropy : reveal only positions whose entropy is below `entropy_tau`,
|
| 94 |
+
an inference-time baseline for dependency-aware revealing
|
| 95 |
+
"""
|
| 96 |
+
B, L = shape
|
| 97 |
+
x = torch.full((B, L), tok.mask, device=device, dtype=torch.long)
|
| 98 |
+
if x_init is not None:
|
| 99 |
+
x = x_init.clone()
|
| 100 |
+
trace = []
|
| 101 |
+
|
| 102 |
+
for s in range(steps, 0, -1):
|
| 103 |
+
masked = x == tok.mask
|
| 104 |
+
if not masked.any():
|
| 105 |
+
break
|
| 106 |
+
logits = model(x)
|
| 107 |
+
probs = logits.softmax(-1)
|
| 108 |
+
if temperature > 0:
|
| 109 |
+
pred = torch.distributions.Categorical(
|
| 110 |
+
logits=logits / temperature).sample()
|
| 111 |
+
conf = probs.gather(-1, pred[..., None]).squeeze(-1)
|
| 112 |
+
else:
|
| 113 |
+
conf, pred = probs.max(-1)
|
| 114 |
+
|
| 115 |
+
if strategy == "random":
|
| 116 |
+
score = torch.rand_like(conf)
|
| 117 |
+
elif strategy == "entropy":
|
| 118 |
+
ent = -(probs * probs.clamp_min(1e-9).log()).sum(-1)
|
| 119 |
+
score = -ent
|
| 120 |
+
else:
|
| 121 |
+
score = conf
|
| 122 |
+
score = score.masked_fill(~masked, -1e9)
|
| 123 |
+
|
| 124 |
+
target_left = int(L * (s - 1) / steps)
|
| 125 |
+
for b in range(B):
|
| 126 |
+
nm = int(masked[b].sum())
|
| 127 |
+
k = max(0, nm - target_left)
|
| 128 |
+
if strategy == "entropy" and entropy_tau is not None and k > 0:
|
| 129 |
+
ent_b = -(probs[b] * probs[b].clamp_min(1e-9).log()).sum(-1)
|
| 130 |
+
allowed = int(((ent_b < entropy_tau) & masked[b]).sum())
|
| 131 |
+
k = max(1, min(k, allowed)) if allowed else 1
|
| 132 |
+
if k:
|
| 133 |
+
idx = score[b].topk(k).indices
|
| 134 |
+
x[b, idx] = pred[b, idx]
|
| 135 |
+
if keep_trace:
|
| 136 |
+
trace.append(x.clone())
|
| 137 |
+
still = x == tok.mask
|
| 138 |
+
if still.any():
|
| 139 |
+
x[still] = pred[still]
|
| 140 |
+
return (x, trace) if keep_trace else x
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@torch.no_grad()
|
| 144 |
+
def nll_bpc(model, batches, tok, device, mc=8):
|
| 145 |
+
"""Monte-Carlo estimate of the MDLM bound, reported as bits per character -
|
| 146 |
+
the standard text8 metric."""
|
| 147 |
+
tot, n = 0.0, 0
|
| 148 |
+
for x0 in batches:
|
| 149 |
+
x0 = x0.to(device)
|
| 150 |
+
B, L = x0.shape
|
| 151 |
+
for _ in range(mc):
|
| 152 |
+
t = (1.0 / L) + (1 - 1.0 / L) * torch.rand(B, device=device)
|
| 153 |
+
m = torch.rand(B, L, device=device) < t[:, None]
|
| 154 |
+
xt = torch.where(m, torch.full_like(x0, tok.mask), x0)
|
| 155 |
+
ce = F.cross_entropy(model(xt).reshape(-1, len(tok)),
|
| 156 |
+
x0.reshape(-1), reduction="none").view(B, L)
|
| 157 |
+
tot += float(((1.0 / t) * (ce * m).sum(1) / L).sum())
|
| 158 |
+
n += B
|
| 159 |
+
return tot / max(n, 1) / torch.log(torch.tensor(2.0)).item()
|
src/model.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Transformer denoiser for masked diffusion.
|
| 2 |
+
|
| 3 |
+
Bidirectional by default (a masked diffusion denoiser must see both sides);
|
| 4 |
+
`causal=True` gives the matched autoregressive baseline. Rotary is the default
|
| 5 |
+
positional encoding - it was the strongest option in our previous study.
|
| 6 |
+
"""
|
| 7 |
+
import math
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def sinusoidal(max_len, d):
|
| 14 |
+
pe = torch.zeros(max_len, d)
|
| 15 |
+
pos = torch.arange(max_len).unsqueeze(1).float()
|
| 16 |
+
div = torch.exp(torch.arange(0, d, 2).float() * (-math.log(10000.0) / d))
|
| 17 |
+
pe[:, 0::2] = torch.sin(pos * div)
|
| 18 |
+
pe[:, 1::2] = torch.cos(pos * div)
|
| 19 |
+
return pe
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def alibi_slopes(n_heads):
|
| 23 |
+
start = 2 ** (-(2 ** -(math.log2(n_heads) - 3))) if math.log2(n_heads).is_integer() else 0.5
|
| 24 |
+
return torch.tensor([start * (start ** i) for i in range(n_heads)])
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def apply_rope(x, cos, sin):
|
| 28 |
+
# x: (B, H, L, Dh)
|
| 29 |
+
x1, x2 = x[..., 0::2], x[..., 1::2]
|
| 30 |
+
rx = torch.stack([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
|
| 31 |
+
return rx.flatten(-2)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class Attention(nn.Module):
|
| 35 |
+
def __init__(self, d, n_heads, pe, causal, max_len):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.h, self.dh, self.pe, self.causal = n_heads, d // n_heads, pe, causal
|
| 38 |
+
self.qkv = nn.Linear(d, 3 * d, bias=False)
|
| 39 |
+
self.proj = nn.Linear(d, d, bias=False)
|
| 40 |
+
if pe == "alibi":
|
| 41 |
+
self.register_buffer("slopes", alibi_slopes(n_heads), persistent=False)
|
| 42 |
+
if pe == "rope":
|
| 43 |
+
inv = 1.0 / (10000 ** (torch.arange(0, self.dh, 2).float() / self.dh))
|
| 44 |
+
ang = torch.arange(max_len).float()[:, None] * inv[None, :]
|
| 45 |
+
self.register_buffer("cos", ang.cos(), persistent=False)
|
| 46 |
+
self.register_buffer("sin", ang.sin(), persistent=False)
|
| 47 |
+
|
| 48 |
+
def forward(self, x, pad_mask, pos_ids=None):
|
| 49 |
+
B, L, D = x.shape
|
| 50 |
+
q, k, v = self.qkv(x).chunk(3, dim=-1)
|
| 51 |
+
q = q.view(B, L, self.h, self.dh).transpose(1, 2)
|
| 52 |
+
k = k.view(B, L, self.h, self.dh).transpose(1, 2)
|
| 53 |
+
v = v.view(B, L, self.h, self.dh).transpose(1, 2)
|
| 54 |
+
|
| 55 |
+
if self.pe == "rope":
|
| 56 |
+
if pos_ids is None:
|
| 57 |
+
cos, sin = self.cos[:L][None, None], self.sin[:L][None, None]
|
| 58 |
+
else:
|
| 59 |
+
cos, sin = self.cos[pos_ids][:, None], self.sin[pos_ids][:, None]
|
| 60 |
+
q, k = apply_rope(q, cos, sin), apply_rope(k, cos, sin)
|
| 61 |
+
|
| 62 |
+
att = (q @ k.transpose(-2, -1)) / math.sqrt(self.dh)
|
| 63 |
+
|
| 64 |
+
if self.pe == "alibi":
|
| 65 |
+
if pos_ids is None:
|
| 66 |
+
pos = torch.arange(L, device=x.device)
|
| 67 |
+
rel = (pos[None, :] - pos[:, None]).abs().float()[None]
|
| 68 |
+
else:
|
| 69 |
+
rel = (pos_ids[:, None, :] - pos_ids[:, :, None]).abs().float()
|
| 70 |
+
att = att - self.slopes.view(1, -1, 1, 1) * rel[:, None]
|
| 71 |
+
|
| 72 |
+
if self.causal:
|
| 73 |
+
cm = torch.ones(L, L, device=x.device, dtype=torch.bool).tril()
|
| 74 |
+
att = att.masked_fill(~cm[None, None], float("-inf"))
|
| 75 |
+
if pad_mask is not None:
|
| 76 |
+
att = att.masked_fill(~pad_mask[:, None, None, :], float("-inf"))
|
| 77 |
+
|
| 78 |
+
att = att.softmax(-1)
|
| 79 |
+
out = (att @ v).transpose(1, 2).reshape(B, L, D)
|
| 80 |
+
return self.proj(out)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class Block(nn.Module):
|
| 84 |
+
def __init__(self, d, n_heads, pe, causal, max_len):
|
| 85 |
+
super().__init__()
|
| 86 |
+
self.n1, self.n2 = nn.LayerNorm(d), nn.LayerNorm(d)
|
| 87 |
+
self.attn = Attention(d, n_heads, pe, causal, max_len)
|
| 88 |
+
self.mlp = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d))
|
| 89 |
+
|
| 90 |
+
def forward(self, x, pad_mask, pos_ids=None):
|
| 91 |
+
x = x + self.attn(self.n1(x), pad_mask, pos_ids)
|
| 92 |
+
return x + self.mlp(self.n2(x))
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class Transformer(nn.Module):
|
| 96 |
+
def __init__(self, vocab, d=384, n_layers=6, n_heads=6, pe="ape", causal=False, max_len=256):
|
| 97 |
+
super().__init__()
|
| 98 |
+
self.pe_kind, self.causal = pe, causal
|
| 99 |
+
self.emb = nn.Embedding(vocab, d)
|
| 100 |
+
# Segment embedding: which part of the equation a token belongs to.
|
| 101 |
+
# Place-value ids deliberately collide across operands and answer, so a
|
| 102 |
+
# bidirectional model needs this to tell those tokens apart; a causal
|
| 103 |
+
# model gets the same information free from the attention mask.
|
| 104 |
+
|
| 105 |
+
if pe == "ape":
|
| 106 |
+
self.pos = nn.Embedding(max_len, d)
|
| 107 |
+
elif pe == "sin":
|
| 108 |
+
self.register_buffer("pos_sin", sinusoidal(max_len, d), persistent=False)
|
| 109 |
+
self.blocks = nn.ModuleList([Block(d, n_heads, pe, causal, max_len) for _ in range(n_layers)])
|
| 110 |
+
self.norm = nn.LayerNorm(d)
|
| 111 |
+
self.head = nn.Linear(d, vocab, bias=False)
|
| 112 |
+
self.apply(self._init)
|
| 113 |
+
|
| 114 |
+
@staticmethod
|
| 115 |
+
def _init(m):
|
| 116 |
+
if isinstance(m, (nn.Linear, nn.Embedding)):
|
| 117 |
+
nn.init.normal_(m.weight, std=0.02)
|
| 118 |
+
if isinstance(m, nn.Linear) and m.bias is not None:
|
| 119 |
+
nn.init.zeros_(m.bias)
|
| 120 |
+
|
| 121 |
+
def forward(self, idx, pad_mask=None, pos_ids=None):
|
| 122 |
+
B, L = idx.shape
|
| 123 |
+
x = self.emb(idx)
|
| 124 |
+
if self.pe_kind == "ape":
|
| 125 |
+
p = torch.arange(L, device=idx.device)[None] if pos_ids is None else pos_ids
|
| 126 |
+
x = x + self.pos(p)
|
| 127 |
+
elif self.pe_kind == "sin":
|
| 128 |
+
p = torch.arange(L, device=idx.device)[None] if pos_ids is None else pos_ids
|
| 129 |
+
x = x + self.pos_sin[p]
|
| 130 |
+
for b in self.blocks:
|
| 131 |
+
x = b(x, pad_mask, pos_ids)
|
| 132 |
+
return self.head(self.norm(x))
|
| 133 |
+
|
| 134 |
+
def n_params(self):
|
| 135 |
+
return sum(p.numel() for p in self.parameters() if p.requires_grad)
|
src/train.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train a masked diffusion LM from scratch and measure quality vs parallelism.
|
| 2 |
+
|
| 3 |
+
The headline axis is accuracy (or bits-per-character) as a function of the
|
| 4 |
+
number of denoising passes. Fewer passes means more tokens committed per pass,
|
| 5 |
+
which is the regime where the conditional-independence assumption bites and
|
| 6 |
+
where every acceleration method must eventually be judged.
|
| 7 |
+
"""
|
| 8 |
+
import argparse, json, math, os, time
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
from data import load_text8, build_probe, CharTokenizer
|
| 13 |
+
from model import Transformer
|
| 14 |
+
import diffusion as dfn
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def get_args():
|
| 18 |
+
p = argparse.ArgumentParser()
|
| 19 |
+
p.add_argument("--objective", choices=["mdlm", "selfcorrupt"], default="mdlm",
|
| 20 |
+
help="mdlm = standard marginal training; selfcorrupt = ours")
|
| 21 |
+
p.add_argument("--k_frac", type=float, default=0.25,
|
| 22 |
+
help="fraction of masked positions committed from the model's "
|
| 23 |
+
"own parallel samples during self-corrupted training")
|
| 24 |
+
p.add_argument("--correct_weight", type=float, default=1.0)
|
| 25 |
+
p.add_argument("--task", choices=["text8", "copy", "agree", "parity"], default="agree")
|
| 26 |
+
p.add_argument("--coupling", type=int, default=4,
|
| 27 |
+
help="probe tasks: how many positions are bound together")
|
| 28 |
+
p.add_argument("--seq_len", type=int, default=128)
|
| 29 |
+
p.add_argument("--n_train", type=int, default=200000)
|
| 30 |
+
p.add_argument("--n_eval", type=int, default=1000)
|
| 31 |
+
p.add_argument("--d", type=int, default=384)
|
| 32 |
+
p.add_argument("--layers", type=int, default=6)
|
| 33 |
+
p.add_argument("--heads", type=int, default=6)
|
| 34 |
+
p.add_argument("--pe", default="rope")
|
| 35 |
+
p.add_argument("--bs", type=int, default=64)
|
| 36 |
+
p.add_argument("--steps", type=int, default=20000)
|
| 37 |
+
p.add_argument("--lr", type=float, default=3e-4)
|
| 38 |
+
p.add_argument("--warmup", type=int, default=500)
|
| 39 |
+
p.add_argument("--seed", type=int, default=0)
|
| 40 |
+
p.add_argument("--data_root", default="data")
|
| 41 |
+
p.add_argument("--eval_bs", type=int, default=100)
|
| 42 |
+
p.add_argument("--out", default="runs/dev")
|
| 43 |
+
return p.parse_args()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# The whole point: sweep the number of passes from fully sequential (L) down to
|
| 47 |
+
# maximally parallel (1) and watch where each method falls over.
|
| 48 |
+
PASS_GRID = [1, 2, 4, 8, 16, 32, 64, 128]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def probe_accuracy(x, task, coupling, tok):
|
| 52 |
+
"""Fraction of sequences satisfying the task constraint exactly."""
|
| 53 |
+
ok = 0
|
| 54 |
+
for row in x.tolist():
|
| 55 |
+
if task == "copy":
|
| 56 |
+
h = len(row) // 2
|
| 57 |
+
ok += row[:h] == row[h:h * 2]
|
| 58 |
+
elif task == "agree":
|
| 59 |
+
groups = [row[s:s + coupling] for s in range(0, len(row), coupling)
|
| 60 |
+
if len(row[s:s + coupling]) == coupling]
|
| 61 |
+
good = all(len(set(g)) == 1 for g in groups) and \
|
| 62 |
+
all(groups[j][0] != groups[j + 1][0] for j in range(len(groups) - 1))
|
| 63 |
+
ok += good
|
| 64 |
+
elif task == "parity":
|
| 65 |
+
groups = [row[s:s + coupling] for s in range(0, len(row), coupling)
|
| 66 |
+
if len(row[s:s + coupling]) >= 2]
|
| 67 |
+
good = True
|
| 68 |
+
for g in groups:
|
| 69 |
+
bits = [v - 2 for v in g]
|
| 70 |
+
if any(b not in (0, 1) for b in bits) or sum(bits[:-1]) % 2 != bits[-1]:
|
| 71 |
+
good = False
|
| 72 |
+
break
|
| 73 |
+
if good:
|
| 74 |
+
good = all(groups[j] != groups[j + 1] for j in range(len(groups) - 1))
|
| 75 |
+
ok += good
|
| 76 |
+
return ok / len(x)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@torch.no_grad()
|
| 80 |
+
def evaluate(model, args, tok, device, eval_data):
|
| 81 |
+
model.eval()
|
| 82 |
+
out = {}
|
| 83 |
+
for steps in PASS_GRID:
|
| 84 |
+
if steps > args.seq_len:
|
| 85 |
+
continue
|
| 86 |
+
accs = []
|
| 87 |
+
for i in range(0, args.n_eval, args.eval_bs):
|
| 88 |
+
b = min(args.eval_bs, args.n_eval - i)
|
| 89 |
+
x_init = None
|
| 90 |
+
if args.task == "copy":
|
| 91 |
+
# Conditional: the first half is GIVEN, so there is one correct
|
| 92 |
+
# completion. Unconditionally a constant sequence copies itself,
|
| 93 |
+
# which any degenerate model satisfies.
|
| 94 |
+
from data import build_probe as _bp
|
| 95 |
+
src, _ = _bp(b, args.seq_len, args.coupling, seed=9000 + i, task="copy")
|
| 96 |
+
x_init = torch.full((b, args.seq_len), tok.mask, device=device, dtype=torch.long)
|
| 97 |
+
h = args.seq_len // 2
|
| 98 |
+
x_init[:, :h] = torch.from_numpy(src[:, :h]).to(device)
|
| 99 |
+
x = dfn.decode(model, (b, args.seq_len), tok, steps, device, x_init=x_init)
|
| 100 |
+
if args.task == "text8":
|
| 101 |
+
accs.append(0.0) # quality for text8 is reported via bpc
|
| 102 |
+
else:
|
| 103 |
+
accs.append(probe_accuracy(x.cpu(), args.task, args.coupling, tok))
|
| 104 |
+
out[str(steps)] = float(np.mean(accs))
|
| 105 |
+
res = {"accuracy_by_passes": out}
|
| 106 |
+
if args.task == "text8" and eval_data is not None:
|
| 107 |
+
res["bpc"] = dfn.nll_bpc(model, eval_data[:8], tok, device)
|
| 108 |
+
model.train()
|
| 109 |
+
return res
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def main():
|
| 113 |
+
a = get_args()
|
| 114 |
+
torch.manual_seed(a.seed); np.random.seed(a.seed)
|
| 115 |
+
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 116 |
+
os.makedirs(a.out, exist_ok=True)
|
| 117 |
+
|
| 118 |
+
if a.task == "text8":
|
| 119 |
+
tr, va, te, tok = load_text8(a.data_root, a.seq_len)
|
| 120 |
+
train = torch.from_numpy(tr)
|
| 121 |
+
eval_data = [torch.from_numpy(va[i:i + a.eval_bs]) for i in range(0, 800, a.eval_bs)]
|
| 122 |
+
else:
|
| 123 |
+
X, tok = build_probe(a.n_train, a.seq_len, a.coupling, seed=a.seed, task=a.task)
|
| 124 |
+
train = torch.from_numpy(X)
|
| 125 |
+
eval_data = None
|
| 126 |
+
|
| 127 |
+
model = Transformer(len(tok), a.d, a.layers, a.heads, a.pe,
|
| 128 |
+
causal=False, max_len=a.seq_len + 8).to(dev)
|
| 129 |
+
print(f"[{a.objective}/{a.task}] params={model.n_params()/1e6:.2f}M "
|
| 130 |
+
f"vocab={len(tok)} seq={a.seq_len} device={dev}", flush=True)
|
| 131 |
+
|
| 132 |
+
opt = torch.optim.AdamW(model.parameters(), lr=a.lr, weight_decay=0.01)
|
| 133 |
+
sched = torch.optim.lr_scheduler.LambdaLR(
|
| 134 |
+
opt, lambda s: min(1.0, (s + 1) / a.warmup) *
|
| 135 |
+
0.5 * (1 + math.cos(math.pi * min(1.0, s / a.steps))))
|
| 136 |
+
amp = dict(device_type="cuda", dtype=torch.bfloat16) if dev == "cuda" \
|
| 137 |
+
else dict(device_type="cpu", enabled=False)
|
| 138 |
+
|
| 139 |
+
t0 = time.time()
|
| 140 |
+
for step in range(a.steps):
|
| 141 |
+
idx = torch.randint(0, len(train), (a.bs,))
|
| 142 |
+
x0 = train[idx].to(dev)
|
| 143 |
+
opt.zero_grad(set_to_none=True)
|
| 144 |
+
with torch.autocast(**amp):
|
| 145 |
+
loss = (dfn.self_corrupted_loss(model, x0, tok, a.k_frac,
|
| 146 |
+
correct_weight=a.correct_weight)
|
| 147 |
+
if a.objective == "selfcorrupt"
|
| 148 |
+
else dfn.mdlm_loss(model, x0, tok))
|
| 149 |
+
loss.backward()
|
| 150 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 151 |
+
opt.step(); sched.step()
|
| 152 |
+
if step % 1000 == 0:
|
| 153 |
+
print(f"step {step:6d} loss {loss.item():.4f} ({time.time()-t0:.0f}s)", flush=True)
|
| 154 |
+
|
| 155 |
+
res = evaluate(model, a, tok, dev, eval_data)
|
| 156 |
+
n_par = model.n_params()
|
| 157 |
+
res.update({"args": vars(a), "minutes": (time.time() - t0) / 60,
|
| 158 |
+
"compute": {"params": n_par,
|
| 159 |
+
"train_tokens": a.steps * a.bs * a.seq_len,
|
| 160 |
+
"train_flops_approx": 6 * n_par * a.steps * a.bs * a.seq_len,
|
| 161 |
+
# self-corruption costs one extra no-grad pass
|
| 162 |
+
"fwd_per_step": 2 if a.objective == "selfcorrupt" else 1}})
|
| 163 |
+
json.dump(res, open(os.path.join(a.out, "result.json"), "w"), indent=2)
|
| 164 |
+
torch.save(model.state_dict(), os.path.join(a.out, "model.pt"))
|
| 165 |
+
print("FINAL", json.dumps(res["accuracy_by_passes"]), flush=True)
|
| 166 |
+
if "bpc" in res:
|
| 167 |
+
print(f"BPC {res['bpc']:.4f}", flush=True)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
if __name__ == "__main__":
|
| 171 |
+
main()
|