GOVINDFROM commited on
Commit
98c193b
·
verified ·
1 Parent(s): 55bc6c4

Automated push from Zaratan

Browse files
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()