Skip to content

Commit aabb4c8

Browse files
committed
feat: implement causal prompt prefill
1 parent 0084125 commit aabb4c8

23 files changed

Lines changed: 177 additions & 958 deletions

README.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,65 @@
11
# d3pm
22

3-
[![ci](https://github.com/dirmeier/block-diffusion-mlx/actions/workflows/ci.yaml/badge.svg)](https://github.com/dirmeier/block-diffusion-mlx/actions/workflows/ci.yaml)
3+
[![ci](https://github.com/dirmeier/d3pm-mlx/actions/workflows/ci.yaml/badge.svg)](https://github.com/dirmeier/d3pm-mlx/actions/workflows/ci.yaml)
44

55
> Discrete denoising diffusion probabilistic models in MLX
66
7+
`d3pm` is a small library for discrete denoising diffusion probabilistic models (D3PM) in
8+
[MLX](https://github.com/ml-explore/mlx).
9+
10+
> [!IMPORTANT]
11+
> ### 🚀 DiffusionGemma
12+
> 🔥 ** `d3pm` now features a 💎 DiffusionGemma 💎 example.**
13+
> DiffusionGemma trains on `tiny_shakespeare` and generates text in a block-autoregressive fashion with a Gemma-4 MoE backbone, uniform (mask-free) diffusion, self-conditioning, and D-CFG guidance. It supports prompt-conditioned generation via causal prompt prefill with bidirectional denoising.
14+
15+
716
## Examples
817

9-
Self-contained examples can be found in [examples](examples/)
18+
Self-contained examples can be found in [examples](examples/).
1019

1120
## Installation
1221

1322
To install the latest GitHub <RELEASE>, just call the following on the command line:
1423

1524
```bash
16-
pip install git+https://github.com/dirmeier/block-diffusion-mlx@<RELEASE>
25+
pip install git+https://github.com/dirmeier/d3pm@<RELEASE>
1726
```
1827

1928
Please do not directly install from the `main` branch, since these commits are not tested, but prefer using PyPI or a stable release.
2029

30+
## Contributing
31+
32+
Contributions in the form of pull requests are more than welcome. A good way to
33+
start is to check out issues labelled
34+
[good first issue](https://github.com/dirmeier/d3pm-mlx/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
35+
36+
In order to contribute:
37+
38+
1) Clone `d3pm-mlx` and install `uv` from [here](https://docs.astral.sh/uv/getting-started/installation/).
39+
2) Install all dependencies using `uv sync --all-groups`.
40+
3) Install the Git hooks:
41+
42+
```bash
43+
uv run pre-commit install -t pre-commit -t commit-msg
44+
```
45+
4) Create a new branch locally, e.g. `git checkout -b feature/my-new-feature`.
46+
5) Implement your contribution and ideally a test case.
47+
6) Check your work (see below).
48+
7) Submit a PR 🙂.
49+
50+
### Development commands
51+
52+
The project uses `uv` for everything:
53+
54+
```bash
55+
uv sync --all-groups
56+
uv run ruff format src examples
57+
uv run ruff check --fix src examples
58+
uv run mypy src examples
59+
uv run pre-commit run --all-files
60+
```
61+
62+
2163
## References
2264

2365
- Arriola et al., [Block Diffusion: Interpolating Between Autoregressive and Diffusion Language Models](https://arxiv.org/abs/2503.09573) (BD3LM), 2025.

examples/diffusiongemma/README.md

Lines changed: 24 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,41 @@
1-
This repository implements the [DiffusionGemma](https://deepmind.google/models/gemma/diffusiongemma/)
2-
denoiser in [MLX](https://github.com/ml-explore/mlx): a *uniform-state* discrete
3-
diffusion language model ([UDLM](https://arxiv.org/abs/2412.10193)) on a Gemma-4
4-
MoE backbone, intended to sit inside the *block-diffusion*
5-
([BD3LM](https://arxiv.org/abs/2503.09573)) generation framework.
1+
# DiffusionGemma
2+
3+
This example trains [DiffusionGemma](https://deepmind.google/models/gemma/diffusiongemma/)
4+
on `tiny_shakespeare`.
65

7-
You can find a minimal experiment in [experiments/diffusiongemma](experiments/diffusiongemma) where we train DiffusionGemma on `tiny_shakespeare`.
8-
For reference on DiffusionGemma itself, see the
9-
[DeepMind implementation](https://github.com/google-deepmind/gemma/tree/main/gemma/diffusion),
10-
the [vLLM writeup](https://github.com/vllm-project/vllm-project.github.io/blob/main/_posts/2026-06-10-diffusion-gemma.md),
11-
and the [NeMo fine-tuning guide](https://github.com/NVIDIA-NeMo/Automodel/blob/main/docs/guides/dllm/diffusiongemma.md).
126
To run the experiment, first download the latest release and install all dependencies via:
137

148
```bash
15-
wget -qO- https://github.com/dirmeier/block-diffusion-mlx/archive/refs/tags/<TAG>.tar.gz | tar zxvf -
9+
wget -qO- https://github.com/dirmeier/d3pm-mlx/archive/refs/tags/<TAG>.tar.gz | tar zxvf -
1610
uv sync --all-groups
1711
```
1812

1913
To train a model and generate some text, call:
2014

2115
```bash
22-
uv run python experiments/diffusiongemma/main.py
16+
uv run python examples/diffusiongemma/main.py
2317
```
2418

25-
Pass `--prompt "..."` to condition generation on a prefix, and `--entropy-bound`
26-
to set the sampler's cumulative acceptance budget. The budget does not scale
27-
with canvas length, so a small or under-trained model on a wide `--block-size`
28-
needs a looser bound (raise it if samples stay noisy).
29-
30-
3119
## Method
3220

33-
DiffusionGemma combines two lines of work from the Kuleshov group: the
34-
block-autoregressive *structure* of BD3LM with the uniform-state *noise* of
35-
UDLM (plus self-conditioning and an entropy-bounded sampler). The axes below
36-
show where each model — and this repository — sits.
37-
38-
| Axis | MDLM | BD3LM | UDLM | Duo | DiffusionGemma | This repo |
39-
|------|------|-------|------|-----|----------------|-----------|
40-
| Noise state | masked | masked | uniform | uniform | uniform (no mask) | **uniform (no mask)** |
41-
| Noise schedule | loglinear | loglinear | loglinear | loglinear || **linear or loglinear (+ importance sampling)** |
42-
| Generation | single block | block-autoregressive | single block | single block | block-autoregressive | **block-autoregressive** |
43-
| Attention | bidirectional | block-causal | bidirectional | bidirectional | causal prefill + bidir. denoise | **block-causal + bidir. denoise** |
44-
| Self-conditioning | no | no | no | yes | yes | **yes (`encode_logits` signal, GeGLU FFN + post-norm)** |
45-
| Guidance ||| D-CFG || D-CFG | **D-CFG (toy label)** |
46-
| Variable length | no | yes | no | no | yes | **yes (growing KV cache)** |
47-
| Backbone | encoder | encoder | encoder | encoder | Gemma-4 MoE | **Gemma-4 MoE** |
21+
The axes below show the model details of the original DiffusionGemma implementation (as well as
22+
several models), and how they compare to `d3pm`'s implementation.
4823

49-
The block-autoregressive loop is implemented: the sampler commits each denoised
50-
block to a growing KV cache and starts the next block conditioned on that
51-
history (`BlockSampler`), and training scores multiple blocks under a
52-
block-causal mask (`block_diffusion_loss`). Two invariants from DeepMind's
53-
reference hold — attention is **bidirectional within a block** (every query
54-
attends to the whole current block, no triangular structure) and
55-
**block-causal across blocks** (block `i` attends to blocks `j <= i`, never
56-
future blocks). That cross-block causality is realised either by an explicit
57-
block-causal mask (parallel processing / training) or implicitly by the
58-
streaming sampler, where future blocks are simply not in the cache yet; the
59-
variable length comes from appending keys/values to the growing cache. An
60-
optional prompt is committed to the cache first (block-causal prefill), so
61-
generation continues from it (`BlockSampler.generate(prompt=...)`).
62-
63-
### Gemma-4 parity
64-
65-
The backbone tracks the reference Gemma-4 MoE block: interleaved
66-
local-sliding / global attention with dual RoPE bases, QK- and value-norm, a
67-
per-block `skip_scale`, a normed-and-scaled MoE router with per-expert scaling
68-
and a shared dense branch, and projection-only per-layer embeddings. The
69-
training recipe uses a UDLM-faithful stratified/antithetic noise sampler with an
70-
optional loglinear schedule and importance-sampling reparametrisation
71-
(transcribed from MDLM `LogLinearNoise`), and the sampler uses an entropy-budget
72-
token-acceptance rule with annealed temperature. Self-conditioning feeds the
73-
previous step's predicted distribution back through the embedding table
74-
(`encode_logits`), matching the reference.
75-
76-
**Deferred (documented):** EMA weights, Duo consistency distillation
77-
([2506.10892](https://arxiv.org/abs/2506.10892)) for few-step sampling, a real
78-
(SentencePiece) tokenizer, and batched multi-sequence sampling.
24+
| Axis | MDLM | BD3LM | UDLM | Duo | DiffusionGemma | `d3pm` package |
25+
|------|------|-------|------|-----|----------------|------|
26+
| Noise state | masked | masked | uniform | uniform | uniform (no mask) | uniform (no mask) |
27+
| Noise schedule | loglinear | loglinear | loglinear | loglinear | ? | linear or loglinear |
28+
| Generation | single block | block-autoregressive | single block | single block | block-autoregressive | block-autoregressive |
29+
| Attention | bidirectional | block-causal | bidirectional | bidirectional | causal prefill + bidir. denoise | causal prefill + bidir. denoise |
30+
| Self-conditioning | no | no | no | yes | yes | yes |
31+
| Guidance ||| D-CFG || D-CFG | D-CFG |
32+
| Variable length | no | yes | no | no | yes | yes |
33+
| Backbone | encoder | encoder | encoder | encoder | Gemma-4 MoE | Gemma-4 MoE |
7934

8035
## Additional references
36+
37+
- [DeepMind reference implementation](https://github.com/google-deepmind/gemma/tree/main/gemma/diffusion)
38+
- [DiffusionGemma model card](https://ai.google.dev/gemma/docs/diffusiongemma/model_card)
39+
- [vLLM integration write-up](https://github.com/vllm-project/vllm-project.github.io/blob/main/_posts/2026-06-10-diffusion-gemma.md)
40+
- [NeMo fine-tuning guide](https://github.com/NVIDIA-NeMo/Automodel/blob/main/docs/guides/dllm/diffusiongemma.md)
41+
- [How to Build a Diffusion Language Model](https://kuleshov-group.github.io/blog/blog/2026/how-to-build-a-diffusion-language-model/), 2026.

examples/diffusiongemma/labels.py

Lines changed: 0 additions & 23 deletions
This file was deleted.

examples/diffusiongemma/main.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import mlx.optimizers as optim
1212
from mlx import nn
1313

14-
import d3pm as dg
14+
import d3pm
1515

1616
if TYPE_CHECKING:
1717
from collections.abc import Callable, Generator
@@ -22,6 +22,17 @@
2222
)
2323

2424

25+
def uppercase_label(
26+
x0: mx.array, block_size: int, threshold: float = 0.3
27+
) -> mx.array:
28+
"""Derive a per-block uppercase label for CFG conditioning."""
29+
b, t = x0.shape
30+
nb = t // block_size
31+
is_upper = (x0 >= ord("A")) & (x0 <= ord("Z"))
32+
frac = is_upper.reshape(b, nb, block_size).mean(axis=-1)
33+
return (frac > threshold).astype(mx.int32)
34+
35+
2536
def load_corpus(path: Path) -> str:
2637
"""Download tiny_shakespeare to ``path`` if missing and read it."""
2738
if not path.exists():
@@ -66,51 +77,50 @@ def main() -> None:
6677
parser.add_argument("--lr", type=float, default=1e-3)
6778
parser.add_argument("--warmup", type=int, default=100)
6879
parser.add_argument("--prompt", type=str, default="Romeo, my Romeo!")
69-
# Total (not per-position) entropy budget for token acceptance; the
70-
# reference uses 0.1 for a confident 26B model, but a small under-trained
71-
# model on a wide canvas needs a looser budget to accept anything.
7280
parser.add_argument("--entropy-bound", type=float, default=1.0)
81+
parser.add_argument("--prompt-len", type=int, default=0)
7382
args = parser.parse_args()
7483

75-
tokenizer = dg.ByteTokenizer()
84+
tokenizer = d3pm.ByteTokenizer()
7685
text = load_corpus(Path("input.txt"))
7786
ids = tokenizer.encode(text)
7887

79-
cfg = dg.ModelConfig(
88+
cfg = d3pm.ModelConfig(
8089
vocab_size=tokenizer.vocab_size,
8190
d_model=256,
8291
num_layers=6,
8392
num_heads=8,
8493
num_kv_heads=2,
8594
head_dim=32,
86-
moe=dg.MoEConfig(num_experts=4, top_k=2, ffn_hidden=512, dense_hidden=256),
95+
moe=d3pm.MoEConfig(
96+
num_experts=4, top_k=2, ffn_hidden=512, dense_hidden=256
97+
),
8798
max_seq_len=args.seq_len,
8899
block_size=args.block_size,
89100
attention_pattern=("local", "global"),
90101
sliding_window_size=args.block_size,
91102
use_qk_norm=True,
92103
)
93-
model = dg.DiffusionGemma(cfg)
94-
process = dg.DiffusionProcess(dg.DiffusionConfig(cfg.vocab_size))
104+
model = d3pm.DiffusionGemma(cfg)
105+
process = d3pm.DiffusionProcess(d3pm.DiffusionConfig(cfg.vocab_size))
95106
opt = optim.AdamW(learning_rate=_lr_schedule(args))
96107

97108
batches = make_batches(ids, args.seq_len, args.batch_size, mx.random.key(0))
98-
# Fixed held-out batch + key: a deterministic, comparable eval loss.
99109
val_x0 = next(
100110
make_batches(ids, args.seq_len, args.batch_size, mx.random.key(7))
101111
)
102112
eval_key = mx.random.key(1234)
103113

104-
sampler = dg.BlockSampler(
114+
sampler = d3pm.BlockSampler(
105115
model,
106116
process,
107-
dg.SamplerConfig(
117+
d3pm.SamplerConfig(
108118
num_steps=64,
109119
seq_len=args.block_size,
110120
entropy_bound=args.entropy_bound,
111121
),
112-
early_stop=dg.ChainedEarlyStop(
113-
[dg.TokenStabilityEarlyStop(), dg.EntropyEarlyStop()]
122+
early_stop=d3pm.ChainedEarlyStop(
123+
[d3pm.TokenStabilityEarlyStop(), d3pm.EntropyEarlyStop()]
114124
),
115125
)
116126
prompt_ids = tokenizer.encode(args.prompt) if args.prompt else None
@@ -128,24 +138,30 @@ def sample_text(sample_key: mx.array, max_blocks: int) -> str:
128138
key, k = mx.random.split(key)
129139

130140
def loss_fn(m: mx.array) -> mx.array:
131-
return dg.block_diffusion_loss(
141+
return d3pm.block_diffusion_loss(
132142
m,
133143
process,
134144
x0, # noqa: B023
135145
k, # noqa: B023
136146
block_size=args.block_size,
147+
prompt_len=args.prompt_len,
137148
)
138149

139150
loss, grads = nn.value_and_grad(model, loss_fn)(model)
140151
opt.update(model, grads)
141152
mx.eval(model.parameters(), opt.state)
142153
if step % 100 == 0:
143-
val = dg.block_diffusion_loss(
144-
model, process, val_x0, eval_key, block_size=args.block_size
154+
val = d3pm.block_diffusion_loss(
155+
model,
156+
process,
157+
val_x0,
158+
eval_key,
159+
block_size=args.block_size,
160+
prompt_len=args.prompt_len,
145161
)
146162
mx.eval(val)
147-
print(f"step {step:5d} train {float(loss):.4f} eval {float(val):.4f}")
148163
key, k = mx.random.split(key)
164+
print(f"step {step:5d} train {float(loss):.4f} eval {float(val):.4f}")
149165
print(f" sample: {sample_text(k, 1)!r}")
150166

151167

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ extend-select = [
9292
"TCH", # Type-checking blocks
9393
"UP", # Pyupgrade (modern syntax)
9494
]
95-
ignore = ["S101", "ANN101"]
95+
ignore = ["S101"]
9696

9797
[tool.ruff.lint.pydocstyle]
9898
convention = "google"

src/d3pm/_src/accept_test.py

Lines changed: 0 additions & 36 deletions
This file was deleted.

0 commit comments

Comments
 (0)