{"id":20629,"date":"2026-08-27T14:05:04","date_gmt":"2026-08-27T14:05:04","guid":{"rendered":"https:\/\/www.comet.com\/site\/?p=20629"},"modified":"2026-08-27T14:05:05","modified_gmt":"2026-08-27T14:05:05","slug":"diffusion-language-models","status":"publish","type":"post","link":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/","title":{"rendered":"Diffusion Language Models, From Scratch to Production"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u201cLanguage model\u201d used to be a very general term for statistical models trained to complete language tasks. It included everything from n-gram models to LSTMs and GRUs. But following the release of GPT-3.5 and ChatGPT, the term \u201clanguage model\u201d has had its definition\u2014at least, from a general audience\u2019s point of view\u2014narrow considerably to specifically mean \u201cautoregressive Transformers\u201d.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In 2026, however, we\u2019ve started to see things broaden a bit. Different architectures have begun to move from the research realm into actual production settings. State-space models, so-called \u201clooped Transformers\u201d, and most prominently, diffusion models are suddenly genuine options for real-world use.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Much of the writing around these models frames the situation as \u201cIs this generally superior to GPT-5.6?\u201d, but this is the wrong way to evaluate them. Each new architecture brings with it particular tradeoffs that are useful in particular situations. The goal of this article is to shine a light on exactly what those tradeoffs are for diffusion language models.<\/p>\n\n\n\n<h2 id=\"h-what-actually-is-a-diffusion-language-model\" class=\"wp-block-heading\">What Actually Is A Diffusion Language Model?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Autoregressive language models generate text one token at a time, left to right. Once a token is generated, it&#8217;s fixed. Diffusion models take a different approach: they generate a whole sequence at once and refine it over multiple passes, so earlier tokens stay open to revision as the rest of the sequence takes shape.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"900\" height=\"506\" src=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/diffusion-text-animation-revision.gif\" alt=\"\" class=\"wp-image-20631\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">That difference (sequential generation versus parallel refinement) can sometimes make diffusion models faster, but also makes them fail differently. And it&#8217;s no longer a purely theoretical tradeoff. <a href=\"https:\/\/deepmind.google\/models\/gemini-diffusion\/\">Google&#8217;s Gemini Diffusion<\/a> remains competitive with autoregressive models on coding benchmarks while generating substantially faster, and its open successor, <a href=\"https:\/\/blog.google\/innovation-and-ai\/technology\/developers-tools\/diffusion-gemma-faster-text-generation\/\">DiffusionGemma<\/a>, runs up to 4x quicker than the autoregressive Gemma 4 it&#8217;s built from, though at some cost in quality.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So the interesting question isn&#8217;t whether diffusion is simply &#8220;better&#8221; than autoregression. It&#8217;s when the speed is worth the quality tradeoff.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We&#8217;ll answer that question in three steps:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Understand the math behind masked diffusion.<\/li>\n\n\n\n<li>Build a tiny diffusion language model from scratch so we can see the mechanism directly.<\/li>\n\n\n\n<li>Evaluate a production diffusion model with Opik to measure the quality\/latency tradeoff rather than assuming it.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Why Diffusion For Text At All<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Generating tokens sequentially carries a structural cost: once a token is emitted, it is never reconsidered. The model never revises an earlier word based on how the sentence turned out.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You\u2019re probably familiar with diffusion models in the context of image generation. An image diffusion model takes a real image, adds Gaussian noise until it is pure static, and trains a network to reverse the process, recovering the image from noise one step at a time. Instead of committing in a single pass, it refines gradually and corrects itself along the way.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"600\" height=\"442\" src=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-diffusion-explainer.gif\" alt=\"\" class=\"wp-image-20632\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This process is commonly referred to as continuous diffusion, and it presents some challenges for language models.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix is to redefine &#8220;adding noise&#8221; as masking. Replace some fraction of the tokens with a [MASK] symbol and train the model to recover them. At a 100% mask rate the sequence is blank; near 0% it is nearly intact, and the model learns to denoise at every level in between. As the next section shows, this is a generalization of BERT&#8217;s masked language modeling objective.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"880\" height=\"342\" src=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/text-masking-explainer-1.gif\" alt=\"\" class=\"wp-image-20633\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Masking gives diffusion two advantages.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first is <strong>parallelism<\/strong>. Because the model predicts masked positions instead of extending a sequence, it can fill many positions at once. This is the speed argument, and the evaluation section below tests it directly.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1000\" height=\"672\" src=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/ar-vs-dllm-comparison.gif\" alt=\"\" class=\"wp-image-20634\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The second is bidirectional context. An autoregressive model attends only to tokens on its left, because the tokens on its right do not exist yet. A diffusion model sees the whole sequence at every step, so it can revise an earlier token as the later ones settle. DeepMind reports that this lets Gemini Diffusion correct errors during generation, which is why diffusion suits editing tasks in math and code. It also frees generation from left-to-right order, which makes infilling and constrained generation more natural, the motivation behind the original Diffusion-LM work (<a href=\"https:\/\/arxiv.org\/abs\/2205.14217\">Li et al., 2022<\/a>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Both advantages are real in principle but show up unevenly in practice. Gemini Diffusion, for instance, matches its autoregressive counterpart on code while trailing it on multilingual and reasoning benchmarks, and parallelism shows why.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To decode in parallel, diffusion models assume the tokens produced in a single step are conditionally independent. In natural language they usually are not, and when the assumption breaks, quality drops. ParallelBench (<a href=\"https:\/\/arxiv.org\/abs\/2510.04767\">Kang et al., 2025<\/a>) shows this degradation is sharp on realistic tasks, and that current decoding strategies cannot adjust their parallelism to task difficulty, making speedup without a quality cost hard to reach. Which advantage wins depends on the task.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"410\" src=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/Screenshot-2026-08-19-at-3.43.18-PM-1024x410.png\" alt=\"\" class=\"wp-image-20635\" srcset=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/Screenshot-2026-08-19-at-3.43.18-PM-1024x410.png 1024w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/Screenshot-2026-08-19-at-3.43.18-PM-300x120.png 300w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/Screenshot-2026-08-19-at-3.43.18-PM-767x307.png 767w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/Screenshot-2026-08-19-at-3.43.18-PM.png 1404w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>LLaDA 1.5 accuracy on ParallelBench as a function of tokens decoded per step. Degradation steepens with token dependency: Copy stays flat, while Shuffle and Words-to-Sentence (hard) collapse as parallelism increases. From the ParallelBench paper (<\/em><a href=\"https:\/\/arxiv.org\/abs\/2510.04767\"><em>Kang et al., 2025<\/em><\/a><em>)<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Underneath both advantages is a single idea: how much intermediary computation each one can do, the variable &#8220;work&#8221; a model puts in before committing to an answer. An autoregressive model does a fixed amount of work per token. When it needs to try harder, it does so by writing more tokens, which is what a long chain of reasoning is. A diffusion model works the other way, spending variable computation on a fixed set of tokens by refining them over more or fewer passes. One tries harder by writing more; the other tries harder by thinking longer about what it has already written.<sup>2<\/sup> If you have used a reasoning model&#8217;s effort settings, you have already met this tradeoff from the outside.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Knowing when to reach for diffusion means understanding how it works from the inside. That is where we start.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Math<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Masked diffusion comes down to three things: how you add noise, how the model removes it, and what it optimizes to learn that. Everything follows from those.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>The forward process<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Take a clean sequence of tokens, <math data-latex=\"x_0 = (x_0^1, \\ldots, x_0^L)\"><semantics><mrow><msub><mi>x<\/mi><mn>0<\/mn><\/msub><mo>=<\/mo><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msubsup><mi>x<\/mi><mn>0<\/mn><mn>1<\/mn><\/msubsup><mo separator=\"true\">,<\/mo><mo>\u2026<\/mo><mo separator=\"true\">,<\/mo><msubsup><mi>x<\/mi><mn>0<\/mn><mi>L<\/mi><\/msubsup><mo form=\"postfix\" stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">x_0 = (x_0^1, \\ldots, x_0^L)<\/annotation><\/semantics><\/math>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The forward process corrupts it by masking. Pick a time <em>t<\/em> between 0 and 1. Under a linear schedule, each token is masked independently with probability <em>t<\/em>, replacing it with a special <code>[MASK]<\/code> symbol <em>m<\/em>.<sup>1<\/sup><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For a single token, the probability of the corrupted value is: <\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><math data-latex=\"q(x_t^i \\mid x_0^i) = (1 - t)\\,\\mathbf{1}[x_t^i = x_0^i] + t\\,\\mathbf{1}[x_t^i = m]\"><semantics><mrow><mi>q<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msubsup><mi>x<\/mi><mi>t<\/mi><mi>i<\/mi><\/msubsup><mo lspace=\"0.22em\" rspace=\"0.22em\" stretchy=\"false\">|<\/mo><msubsup><mi>x<\/mi><mn>0<\/mn><mi>i<\/mi><\/msubsup><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>=<\/mo><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mn>1<\/mn><mo>\u2212<\/mo><mi>t<\/mi><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mspace width=\"0.1667em\"><\/mspace><mn>\ud835\udfcf<\/mn><mo form=\"prefix\" stretchy=\"false\">[<\/mo><msubsup><mi>x<\/mi><mi>t<\/mi><mi>i<\/mi><\/msubsup><mo>=<\/mo><msubsup><mi>x<\/mi><mn>0<\/mn><mi>i<\/mi><\/msubsup><mo form=\"postfix\" stretchy=\"false\">]<\/mo><mo>+<\/mo><mi>t<\/mi><mspace width=\"0.1667em\"><\/mspace><mn>\ud835\udfcf<\/mn><mo form=\"prefix\" stretchy=\"false\">[<\/mo><msubsup><mi>x<\/mi><mi>t<\/mi><mi>i<\/mi><\/msubsup><mo>=<\/mo><mi>m<\/mi><mo form=\"postfix\" stretchy=\"false\">]<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">q(x_t^i \\mid x_0^i) = (1 &#8211; t)\\,\\mathbf{1}[x_t^i = x_0^i] + t\\,\\mathbf{1}[x_t^i = m]<\/annotation><\/semantics><\/math><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In words: with probability 1\u2212<em>t<\/em> the token is left alone, and with probability <em>t<\/em> it becomes <code>[MASK]<\/code>. So <em>t<\/em> is a dial for how corrupted the text is, fully intact at 0, fully masked at 1.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the discrete analogue of adding Gaussian noise to an image, where <em>t<\/em> plays the same role the noise level does for an image. The difference is that &#8220;noise&#8221; here is not a small perturbation of a value.It is the total erasure of a token, replaced by a single placeholder symbol.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>The reverse process<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Generation runs this backwards. The model is a denoiser: it takes a partially masked sequence <em>x<sub>t<\/sub><\/em> and predicts the original token at every masked position at once. Call i <math data-latex=\"p_\\theta(x_0^i \\mid x_t)\"><semantics><mrow><msub><mi>p<\/mi><mi>\u03b8<\/mi><\/msub><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msubsup><mi>x<\/mi><mn>0<\/mn><mi>i<\/mi><\/msubsup><mo lspace=\"0.22em\" rspace=\"0.22em\" stretchy=\"false\">|<\/mo><msub><mi>x<\/mi><mi>t<\/mi><\/msub><mo form=\"postfix\" stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">p_\\theta(x_0^i \\mid x_t)<\/annotation><\/semantics><\/math>, the model&#8217;s distribution over what the true token was, for each masked position <em>i<\/em>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two properties matter here. The model conditions on the entire sequence <em>x<sub>t<\/sub><\/em>, both the masked positions and the unmasked ones on either side. It attends in both directions. And it predicts all masked positions in a single forward pass, which is where the parallelism comes from.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That second property is easy to misread. Predicting all masked positions at once does not mean predicting them independently of one another in the final output. A single denoising step does treat the masked positions as conditionally independent given <em>x<\/em><em><sub>t<\/sub><\/em>, the assumption that makes the parallel prediction tractable, but generation is not a single step. It runs many steps, and each step reveals a few tokens and feeds them back as context for the next. The dependencies between tokens are reintroduced across steps, through the model re-attending to what it has already committed. This is exactly the trade the ParallelBench work probes: commit too many tokens per step and you lean too hard on the independence assumption; commit too few and you lose the speed that made diffusion attractive.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>The training objective<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The model learns by trying to reverse the corruption. Sample a clean sequence, sample a time <em>t<\/em>, mask the sequence accordingly, and ask the model to predict the original tokens at the masked positions. Score it with cross-entropy, evaluated only where tokens were masked:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><math data-latex=\"\\mathcal{L} = -\\,\\mathbb{E}_{t,\\,x_0,\\,x_t}\\left[ w(t) \\sum_{i=1}^{L} \\mathbf{1}[x_t^i = m] \\log p_\\theta(x_0^i \\mid x_t) \\right]\"><semantics><mrow><mi class=\"mathcal\">\u2112<\/mi><mo>=<\/mo><mo form=\"prefix\" stretchy=\"false\">\u2212<\/mo><mspace width=\"0.1667em\"><\/mspace><msub><mi>\ud835\udd3c<\/mi><mrow><mi>t<\/mi><mo separator=\"true\">,<\/mo><mspace width=\"0.1667em\"><\/mspace><msub><mi>x<\/mi><mn>0<\/mn><\/msub><mo separator=\"true\">,<\/mo><mspace width=\"0.1667em\"><\/mspace><msub><mi>x<\/mi><mi>t<\/mi><\/msub><\/mrow><\/msub><mrow><mo fence=\"true\" form=\"prefix\">[<\/mo><mi>w<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mi>t<\/mi><mo form=\"postfix\" stretchy=\"false\">)<\/mo><msubsup><mo movablelimits=\"false\">\u2211<\/mo><mrow><mi>i<\/mi><mo>=<\/mo><mn>1<\/mn><\/mrow><mi>L<\/mi><\/msubsup><mn>\ud835\udfcf<\/mn><mo form=\"prefix\" stretchy=\"false\">[<\/mo><msubsup><mi>x<\/mi><mi>t<\/mi><mi>i<\/mi><\/msubsup><mo>=<\/mo><mi>m<\/mi><mo form=\"postfix\" stretchy=\"false\">]<\/mo><mrow><mspace width=\"0.1667em\"><\/mspace><mi>log<\/mi><mo>\u2061<\/mo><mspace width=\"0.1667em\"><\/mspace><\/mrow><msub><mi>p<\/mi><mi>\u03b8<\/mi><\/msub><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msubsup><mi>x<\/mi><mn>0<\/mn><mi>i<\/mi><\/msubsup><mo lspace=\"0.22em\" rspace=\"0.22em\" stretchy=\"false\">|<\/mo><msub><mi>x<\/mi><mi>t<\/mi><\/msub><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo fence=\"true\" form=\"postfix\">]<\/mo><\/mrow><\/mrow><annotation encoding=\"application\/x-tex\">\\mathcal{L} = -\\,\\mathbb{E}_{t,\\,x_0,\\,x_t}\\left[ w(t) \\sum_{i=1}^{L} \\mathbf{1}[x_t^i = m] \\log p_\\theta(x_0^i \\mid x_t) \\right]<\/annotation><\/semantics><\/math><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The indicator restricts the loss to masked positions; the model gets no credit for copying tokens it was handed. The weight <em>w<\/em>(<em>t<\/em>) rescales the loss by time step. With the weighting the objective derives to, the summed loss is a variational bound on the sequence log-likelihood, which is what lets you sample from the trained model as a generator rather than use it only for fill-in-the-blank prediction like BERT. In practice, implementations sometimes simplify or adjust this weighting for stability, trading a little theoretical tightness for easier training. This is the simplified form from the MDLM work (<a href=\"https:\/\/arxiv.org\/abs\/2406.07524\">Sahoo et al., 2024<\/a>), whose full derivation is worth reading there; the object you actually optimize is the weighted masked cross-entropy above.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If that objective looks familiar, it should. Masking tokens and predicting them from bidirectional context is BERT&#8217;s masked language modeling objective. The difference is that BERT masks at a single fixed rate, around 15%, while masked diffusion trains across every masking rate from 0 to 100%, sampled through <em>t<\/em>. <strong>Masked diffusion is masked language modeling extended over a continuum of corruption levels, which turns a representation-learning objective into a generative one.<\/strong> Train BERT to fill in blanks at every possible density of blanks, and you can generate by starting from all blanks and filling them in.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The next section builds exactly this: a bidirectional encoder trained with masked cross-entropy, sampled by starting from a fully masked sequence and denoising it step by step.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Building a Diffusion LM From Scratch<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The model here is deliberately tiny: character-level, trained on a handful of sentences, small enough to run in a notebook on CPU in a couple of minutes. It will only ever reassemble the few sentences it was trained on, but that is enough to see the mechanism work. The masking, the bidirectional denoiser, and the step-by-step sampling from the previous section all become concrete in the code below. You can run it and see for yourself how the step count changes the output.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Follow along with the Colab:<\/strong> <a href=\"https:\/\/colab.research.google.com\/drive\/1UgA3udHN0dznuGTkmeCLzZCZjjkyhc7e\">https:\/\/colab.research.google.com\/drive\/1UgA3udHN0dznuGTkmeCLzZCZjjkyhc7e<\/a><\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Data and vocabulary<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Start with a small corpus of simple sentences and a character-level vocabulary. The only unusual piece is the mask token: it gets an <code>id<\/code> one past the real characters, so the model has a symbol that means &#8220;something was here.&#8221;<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>text = (\n    \"the cat sat on the mat. \"\n    \"the dog ran in the park. \"\n    \"a bird flew over the tree. \"\n    \"the fish swam in the pond. \"\n    \"the sun set behind the hill. \"\n) * 200\n\nchars = sorted(set(text))\nstoi = {c: i for i, c in enumerate(chars)}\nitos = {i: c for c, i in stoi.items()}\nMASK = len(chars)          # mask token id: one past the real vocab\nVOCAB = len(chars) + 1     # real characters + &#91;MASK]\nBLOCK = 24<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>The forward process in code<\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">This is the masking equation from the previous section, written directly. Sample a masking rate <code>t<\/code> per sequence, then mask each token independently with probability <code>t<\/code>. That comparison, <code>torch.rand_like(x) &lt; t<\/code>, masks each position with probability <code>t<\/code>, which is the linear schedule from the math: the mask rate is just t itself.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def add_noise(x):\n    # forward process: this is q(x_t | x_0) from the math, applied to a whole batch\n    t = torch.rand(x.size(0), 1)                        # sample a masking rate t ~ U(0,1) per sequence\n    mask = torch.rand_like(x, dtype=torch.float) &lt; t    # mask each token independently with probability t\n    xt = torch.where(mask, torch.full_like(x, MASK), x) # masked -> &#91;MASK], else keep original\n    return xt, mask, t.squeeze(1)                       # mask marks which positions the loss will score\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The returned <code>mask<\/code> tells us which positions were corrupted, which is all the training loop needs to score the model only where it had to guess.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>The denoiser<\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">The model is a standard transformer encoder. There is one line that makes it a diffusion model rather than a language model in the usual sense, and it is a line you do not write: there is no causal mask. The encoder attends in both directions, so every position sees the whole sequence, exactly the bidirectional property from the math.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class Denoiser(nn.Module):\n    def __init__(self, vocab=VOCAB, d=64, heads=4, layers=2, block=BLOCK):\n        super().__init__()\n        self.tok = nn.Embedding(vocab, d)\n        self.pos = nn.Embedding(block, d)\n        self.time = nn.Sequential(nn.Linear(1, d), nn.GELU(), nn.Linear(d, d))\n        enc = nn.TransformerEncoderLayer(d, heads, dim_feedforward=4*d,\n                                         batch_first=True,activation=\"gelu\")\n        self.blocks = nn.TransformerEncoder(enc, layers)\n        self.ln = nn.LayerNorm(d)\n        self.head = nn.Linear(d, vocab)\n\n    def forward(self, x, t):\n        pos = torch.arange(x.size(1))\n        h = self.tok(x) + self.pos(pos)&#91;None]      \n        h = h + self.time(t&#91;:, None])&#91;:, None, :]   # condition on noise level: same t as the forward process\n        h = self.blocks(h)                          # no causal mask: every position attends both directions\n        return self.head(self.ln(h))                # logits over vocab at every position = p_theta(x_0 | x_t)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you have built a GPT-style model before, this is that same architecture with the causal mask removed. That single change, letting every position see the whole sequence, is what makes it a diffusion denoiser instead of an autoregressive model.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>The training loop<\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">The training loop is simply the loss equation from the previous section, turned into code: corrupt a batch, predict the originals, and take cross-entropy on the masked positions only.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def train(steps=2500, lr=3e-4):\n    model = Denoiser()\n    opt = torch.optim.AdamW(model.parameters(), lr=lr)\n    for step in range(steps):\n        x = get_batch()\n        xt, mask, t = add_noise(x)\n        logits = model(xt, t)\n        # cross-entropy only on masked positions: the indicator 1&#91;x_t = MASK] from the loss equation\n        loss = F.cross_entropy(logits&#91;mask], x&#91;mask])   # loss only where masked\n        opt.zero_grad(); loss.backward(); opt.step()\n    return model<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>logits[mask]<\/code> indexing does the job of the indicator in the loss equation: it selects only the masked positions, so the model is scored on what it had to guess and gets nothing for the tokens it was handed.&#8221; Running this drops the loss from about 3.3 to around 1.0 in a couple of minutes on CPU.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>The sampling loop<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the reverse process, and it is where the step-count knob lives. Start from a fully masked sequence and denoise it over n_steps iterations. At each step, the model predicts every masked position, and we commit only a fraction of them, (the ones the model is most confident about) feeding those back as context for the next step. Committing the most confident predictions first is what lets each step build on the tokens the previous steps got right.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@torch.no_grad()\ndef sample(model, n_steps, length=BLOCK):\n    # reverse process: start fully masked, then unmask a few positions at a time over n_steps\n    x = torch.full((1, length), MASK, dtype=torch.long) # x at t=1: everything masked\n    reveal_per_step = math.ceil(length \/ n_steps)       # how many to commit each step\n    for s in range(n_steps):\n        t = torch.tensor(&#91;(x&#91;0] == MASK).float().mean()])   # current noise level = fraction still masked\n        probs = F.softmax(model(x, t), -1)&#91;0]               # p_theta(x_0 | x_t) at every position\n        pred = torch.multinomial(probs, 1).squeeze(-1)      # sample a token per position\n        conf = probs.gather(-1, pred&#91;:, None]).squeeze(-1)  # model's confidence in each sampled token\n\n        masked = (x&#91;0] == MASK)\n        if masked.sum() == 0:\n            break\n        # remasking strategy: keep only the most confident predictions, leave the rest masked for next step\n        cand = torch.where(masked, conf, torch.full_like(conf, -1.0))\n        k = min(reveal_per_step, int(masked.sum().item()))\n        idx = torch.topk(cand, k).indices\n        x&#91;0, idx] = pred&#91;idx]                               # commit those tokens; they become context next step\n    # fill any positions still masked after the last step\n    left = (x&#91;0] == MASK)\n    if left.any():\n        x&#91;0, left] = model(x, torch.tensor(&#91;0.0]))&#91;0, left].argmax(-1)\n    return \"\".join(itos&#91;i.item()] for i in x&#91;0])<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two knobs control everything here: <code>n_steps<\/code>, the number of denoising iterations, and the rule for how many positions to reveal per step. Both trade quality against speed, and both are exactly what a production diffusion model exposes, just at a larger scale.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Turning the step-count knob<\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Here is what that produces, the same trained model sampled at 2, 8, and 24 steps:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91; 2 steps] 'he on the ohta troc ' \n&#91; 8 steps] '. the the sat on the mat' \n&#91;24 steps] '. the dog ran in the par'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">At 2 steps the model commits almost everything at once, leaning entirely on the conditional-independence assumption, and the result has the right letter and spacing statistics but no structure. At 8 steps whole words hold together and a sentence starts to form. At 24 steps, one step per token, it recovers a clean sentence from the training text, &#8220;the dog ran in the par&#8230;&#8221;, because each step commits only a few positions and lets the rest condition on them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the core diffusion tradeoff, visible in miniature: more steps, better output, more compute. It is the same curve the evaluation section measures on production models, and here you can see exactly where it comes from. You can see the independence assumption from the math directly in the 2-step sample: commit that many tokens at once and it falls apart.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One thing this model does <em>not<\/em> do is generate novel text. It has five sentences of training data and a few dozen characters of vocabulary, so when it produces something clean, it is reassembling sentences it has seen, not composing new ones. That is the difference between a teaching artifact and a working model. The mechanism we built is exactly what a production diffusion model uses: masking, a bidirectional denoiser, and iterative unmasking.What a production model adds is scale: enough parameters and data to generalize instead of memorize. The next section evaluates models that have made that jump.<br><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Evaluating production diffusion LLMs with Opik<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The toy model showed the mechanism. It cannot tell you whether diffusion is worth using, because it only memorizes a handful of sentences and runs on a CPU. To find out, we evaluate a production diffusion model the way you would evaluate any model headed for production: run it on a fixed task, trace every call, and score the outputs on the things you actually care about.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">An autoregressive model and a diffusion model are two ways to spend intermediary computation, and they spend it differently: an autoregressive model does more work by writing more tokens, while a diffusion model does more work per token. That difference tells us what to measure. Quality says whether the answer is right; latency and output tokens say what each model spent to get there and whether the two paradigms spend differently in practice.We compare one diffusion model, Inception&#8217;s Mercury 2, against a cost-matched autoregressive baseline, Claude Haiku 4.5, on a set of small code-generation tasks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We use code generation because it&#8217;s verifiable: the output either passes a test or it doesn&#8217;t.<\/p>\n\n\n\n<h3 id=\"h-the-setu-p\" class=\"wp-block-heading\"><strong>The setu<\/strong>p<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Follow along in the Colab here:<\/strong> <a href=\"https:\/\/colab.research.google.com\/drive\/1BGfRh4W-ULOTa-U659ZvY-cYTkPGzEc_\">https:\/\/colab.research.google.com\/drive\/1BGfRh4W-ULOTa-U659ZvY-cYTkPGzEc_<\/a>&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can follow along in the <a href=\"https:\/\/colab.research.google.com\/drive\/1BGfRh4W-ULOTa-U659ZvY-cYTkPGzEc_\">companion notebook<\/a>. It uses Opik&#8217;s hosted free tier by default, so it runs without any setup. If you&#8217;d rather self-host the open-source version, follow the instructions in the <a href=\"https:\/\/github.com\/comet-ml\/opik\">Opik repo<\/a> and update the <code>OPIK_URL<\/code> variable at the top of the notebook to point at your local instance.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Both models are called through LiteLLM, which gives them the same interface, so the only thing that changes between runs is the model name. Opik handles the tracing. Wrapping the LiteLLM call with track_completion means every request is logged automatically, with its latency and token usage attached.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tracked_completion = track_completion()(litellm.completion)\n\ndef generate(prompt: str, model: str, **kwargs) -> dict:\n    t0 = time.time()\n    resp = tracked_completion(\n        model=model,\n        messages=&#91;{\"role\": \"user\", \"content\": prompt}],\n        **kwargs,\n    )\n    return {\n        \"text\": resp.choices&#91;0].message.content or \"\",\n        \"latency_s\": time.time() - t0,\n        \"output_tokens\": getattr(resp.usage, \"completion_tokens\", None),\n    }<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"580\" src=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-1024x580.png\" alt=\"\" class=\"wp-image-20636\" style=\"aspect-ratio:1.7677053824362605;width:624px;height:auto\" srcset=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-1024x580.png 1024w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-300x170.png 300w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-767x434.png 767w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-1536x869.png 1536w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>A single traced call in Opik: the prompt and response along with the latency and token counts captured automatically, with no extra logging code.<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The tasks are graded into easy, medium, and hard tiers, so we can see how each model behaves as problems get harder. Each prompt pins the function name so the test knows what to call.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>The evaluation metric<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We run the model&#8217;s code against the task&#8217;s test, and score 0 for anything that raises an exception. Before running it, we strip the Markdown fences models wrap code in, since they aren&#8217;t valid Python, and because each prompt pins the function name, the test can always find the function to call.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class CodePasses(base_metric.BaseMetric):\n    def __init__(self, name: str = \"code_passes\"):\n        self.name = name\n\n    def score(self, output: str, test: str, **kwargs):\n        try:\n            scope = {}\n            exec(extract_code(output), scope)\n            exec(test, scope)\n            passed = True\n        except Exception:\n            passed = False\n        return score_result.ScoreResult(name=self.name,\n                                        value=1.0 if passed else 0.0)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The evaluation task itself returns only what the metric scores. The other measurements, latency, output tokens, and difficulty, are logged to the Opik trace, where they can be compared and filtered without interfering with scoring.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def eval_task(item, model, **gen_kwargs):\n    r = generate(item&#91;\"prompt\"], model, **gen_kwargs)\n    opik_context.update_current_trace(\n        metadata={\"difficulty\": item&#91;\"difficulty\"]},\n        feedback_scores=&#91;\n            {\"name\": \"latency_s\", \"value\": float(r&#91;\"latency_s\"])},\n            {\"name\": \"output_tokens\", \"value\": float(r&#91;\"output_tokens\"] or 0)},\n        ],\n    )\n    return {\"output\": r&#91;\"text\"], \"test\": item&#91;\"test\"]}<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"555\" src=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-1024x555.jpeg\" alt=\"\" class=\"wp-image-20637\" style=\"aspect-ratio:1.8407079646017699;width:624px;height:auto\" srcset=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-1024x555.jpeg 1024w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-767x416.jpeg 767w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-300x163.jpeg 300w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image.jpeg 1376w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>Each task scored by the<\/em> <em><code>code_passes<\/code><\/em> <em>metric, pass or fail from running the model&#8217;s code against the test, logged per item so it can be aggregated and broken down by difficulty.<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">For open-ended or non-verifiable tasks, we could swap our pass\/fail metric for one of Opik\u2019s built in LLM-as-a-judge metrics and run them alongside the <code>latency<\/code> and <code>output_tokens <\/code>logged as feedback scores above.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Comparing diffusion and autoregressive LMs<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Running the evaluation over all thirty tasks, averaged across twenty runs, gives a clearer picture than a single pass would. Overall, Mercury 2 answered about twice as fast as Haiku (3.0s versus 6.0s on average) while passing fewer tasks (84% versus 99.7%). But the overall number hides the more interesting result, which only shows up when you break performance down by difficulty.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"611\" src=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-1-1024x611.png\" alt=\"\" class=\"wp-image-20638\" style=\"aspect-ratio:1.6774193548387097;width:624px;height:auto\" srcset=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-1-1024x611.png 1024w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-1-766x457.png 766w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-1-300x179.png 300w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-1.png 1496w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The quality gap is almost entirely on hard tasks. On easy problems the two models are identical, both at 100%. On medium ones Mercury slips slightly, 89% to Haiku&#8217;s 100%. It is only on the hard tier that they diverge sharply: Mercury passes 60% where Haiku passes 99%. So this isn&#8217;t a model that&#8217;s uniformly a bit worse. It&#8217;s a model that matches a strong autoregressive baseline until the problem gets genuinely hard, then falls off.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The speed advantage, meanwhile, holds at every level, and widens with difficulty.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"597\" src=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-2-1024x597.png\" alt=\"\" class=\"wp-image-20639\" style=\"aspect-ratio:1.7142857142857142;width:624px;height:auto\" srcset=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-2-1024x597.png 1024w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-2-766x447.png 766w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-2-300x175.png 300w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-2.png 1526w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Mercury was faster on every tier, and the gap grew as tasks got harder: roughly 1.9s versus 3.5s on easy tasks, but 4.5s versus 8.8s on hard ones. Both models spent more time and more tokens on harder problems, and interestingly, they spent them similarly, output token counts were nearly identical between the two (689 versus 714 on average). The difference wasn&#8217;t in how much either model wrote; it was that Haiku&#8217;s extra work on hard tasks cost roughly twice the wall-clock time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Which model you&#8217;d choose comes down to where your tasks fall. For work that stays in easy-to-medium territory, Mercury gives you the same quality at half the latency, a clear win. For work heavy in genuinely hard problems, Haiku&#8217;s near-perfect accuracy is worth its slower responses, unless a wrong answer is cheap to catch, in which case Mercury&#8217;s speed may still be the better trade.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Turning the effort dial<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Mercury 2 exposes a <code>reasoning_effort<\/code> setting, its control for how much computation to spend per response, from instant up to high. The question the toy model raised was whether spending more computation produces better answers. Sweeping all four levels across the same thirty tasks answers it, and the answer is more nuanced than a simple yes or no.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"657\" src=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-3-1024x657.png\" alt=\"\" class=\"wp-image-20640\" style=\"aspect-ratio:1.56;width:624px;height:auto\" srcset=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-3-1024x657.png 1024w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-3-300x192.png 300w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-3-767x492.png 767w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-3-1536x986.png 1536w, https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/image-3.png 1624w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">For most of the range, turning the dial up does very little. Across <code>instant<\/code>, <code>low<\/code>, and <code>medium<\/code>, hard-task pass rate barely moves, hovering around 58%, while latency drifts up only slightly. Then at <code>high<\/code>, something changes: pass rate on hard tasks jumps to 83%, a 25-point gain, and latency climbs with it, from roughly 4.5s to 7.1s.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two things are worth drawing out. First, the extra computation does convert into better answers, but only at the top of the range and only where the problem is hard enough to use it. On easy and medium tasks, higher effort barely changed the outcome, those problems were already solved at low effort, so more computation was just more latency. The gains concentrated exactly where the model was struggling. Second, the payoff is discontinuous: three of the four settings behave almost identically, and all the improvement is packed into the jump to <code>high<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the intermediary-computation idea made concrete. Mercury&#8217;s lever for spending more work is real, and when a hard problem demands it, pushing that lever to its limit recovers a large share of the accuracy the model otherwise loses. It just doesn&#8217;t do much until you push it all the way, and on easy work there&#8217;s nothing to recover, so the spend is wasted.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Reading the results honestly<\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">The latency figures are end-to-end API times, which include network and provider-side serving, not just each model&#8217;s own compute, so they reflect what you&#8217;d experience as a user rather than a clean measurement of the paradigm. And <code>reasoning_effort<\/code> is a black box: Inception doesn&#8217;t document what it changes internally, so we can measure what it costs and returns, but not what it does under the hood.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">None of this is settled, and the tradeoff isn&#8217;t specific to this one comparison. A June 2026 analysis benchmarked eight diffusion LLMs and showed that quality and cost trade off differently depending on denoising steps, block size, and how aggressively you unmask in parallel: more compute helped coding up to a point, saturated on math, and actively hurt translation. Block size cuts the same way, with small blocks refining well but decoding slowly and large blocks parallelizing at the cost of premature commitments. The point of instrumenting these runs is not to crown a winner but to see where on that surface your workload lands.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">When Diffusion LMs Make Sense<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Whether diffusion wins depends on the task, and our evaluation shows the shape of it, though what it directly establishes is narrow. On code-generation tasks, Mercury 2 matched a strong autoregressive baseline on easy and medium problems while halving latency, then fell behind on the hardest ones, where more computation helped only at the highest effort setting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The dial Inception exposes is one instance of a larger pattern. <a href=\"https:\/\/arxiv.org\/abs\/2602.05859\">A recent interpretability paper<\/a> compared decoding orders on Dream-7B and found that decoding random positions first scored 8% on GSM8K while decoding the most confident positions first scored 56 to 59%, a far wider swing than any setting on a production API will show you. How you run a diffusion model matters as much as which one you pick.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From there, the mechanism suggests where the pattern should generalize. Diffusion&#8217;s advantage is parallelism, so it should pay off where latency is the binding constraint and the output is short enough to resolve in a few passes: code completion, on-device inference, high-throughput serving. The weakness is the mirror image, long, dependent generation, where the conditional-independence assumption behind parallel decoding breaks down and an autoregressive model&#8217;s left-to-right commitment becomes a feature rather than a limitation. Streaming is awkward for a related reason: a diffusion model refines the whole sequence at once, so there&#8217;s no natural way to emit it token by token. These claims extend past what we measured, but they follow from the same tradeoff the evaluation made concrete.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Diffusion is not a replacement for autoregressive generation, and the people building it are not claiming it is. Google&#8217;s own guidance for <a href=\"https:\/\/blog.google\/innovation-and-ai\/technology\/developers-tools\/diffusion-gemma-faster-text-generation\/\">DiffusionGemma<\/a> says as much: autoregressive for maximum quality, diffusion for speed-critical work. Our evaluation reached the same conclusion from the other direction. Diffusion is a different way to spend computation, faster when the work parallelizes, weaker when it doesn&#8217;t, and worth understanding because that tradeoff is becoming one you can choose.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Footnotes<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><sup>1<\/sup>&#8220;Writing more tokens&#8221; here means the reasoning tokens a model generates to work through a problem, not the explanation it may produce afterward to justify its answer. The former is computation toward the answer; the latter may be at least partly presentation rather than additional reasoning. There&#8217;s an active line of work suggesting the gap can be wider still, that even a model&#8217;s stated reasoning is not always a faithful account of its internal computation (<a href=\"https:\/\/www.alphaxiv.org\/abs\/2507.chain-of-thought-is-not-explainability\">Barez et al., 2025<\/a>; <a href=\"https:\/\/arxiv.org\/abs\/2512.23032\">Zaman et al., 2025<\/a> ; <a href=\"https:\/\/arxiv.org\/abs\/2503.08679\">Arcuschin et al., 2025<\/a>, <a href=\"https:\/\/arxiv.org\/abs\/2505.05410\">Chen et al., 2025<\/a>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><sup>&nbsp;2<\/sup>Under the linear schedule the masking probability equals <em>t<\/em> directly, which keeps the notation clean. More generally the probability is 1\u2212<em>\u03b1<\/em><em><sub>t<\/sub><\/em>\u200b for a monotonically decreasing schedule <em>\u03b1<\/em><em><sub>t<\/sub><\/em>\u200b, and the loss weight <em>w<\/em>(<em>t<\/em>) depends on that choice. The linear case is enough to build and understand the model; see <a href=\"https:\/\/arxiv.org\/abs\/2406.07524\">Sahoo et al. (2024)<\/a> for the general treatment.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u201cLanguage model\u201d used to be a very general term for statistical models trained to complete language tasks. It included everything from n-gram models to LSTMs and GRUs. But following the release of GPT-3.5 and ChatGPT, the term \u201clanguage model\u201d has had its definition\u2014at least, from a general audience\u2019s point of view\u2014narrow considerably to specifically mean [&hellip;]<\/p>\n","protected":false},"author":22,"featured_media":20643,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"customer_name":"","customer_description":"","customer_industry":"","customer_technologies":"","customer_logo":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[12,7],"tags":[40,60,71,52,94],"coauthors":[133],"class_list":["post-20629","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-thought-leadership","category-tutorials","tag-comet","tag-diffusion-models","tag-language-models","tag-llm","tag-opik"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.9 (Yoast SEO v25.9) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Diffusion Language Models, From Scratch to Production - Comet<\/title>\n<meta name=\"description\" content=\"Diffusion LLMs trade quality for speed. We build one from scratch and evaluate Mercury 2 against Claude Haiku with Opik to measure exactly when it&#039;s worth it\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Diffusion Language Models, From Scratch to Production\" \/>\n<meta property=\"og:description\" content=\"Diffusion LLMs trade quality for speed. We build one from scratch and evaluate Mercury 2 against Claude Haiku with Opik to measure exactly when it&#039;s worth it\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/\" \/>\n<meta property=\"og:site_name\" content=\"Comet\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/cometdotml\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-27T14:05:04+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-08-27T14:05:05+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2025\/10\/Share-image-3.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1800\" \/>\n\t<meta property=\"og:image:height\" content=\"945\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Abby Morgan\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@anmorgan2414\" \/>\n<meta name=\"twitter:site\" content=\"@Cometml\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Abby Morgan\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"21 minutes\" \/>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Diffusion Language Models, From Scratch to Production - Comet","description":"Diffusion LLMs trade quality for speed. We build one from scratch and evaluate Mercury 2 against Claude Haiku with Opik to measure exactly when it's worth it","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/","og_locale":"en_US","og_type":"article","og_title":"Diffusion Language Models, From Scratch to Production","og_description":"Diffusion LLMs trade quality for speed. We build one from scratch and evaluate Mercury 2 against Claude Haiku with Opik to measure exactly when it's worth it","og_url":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/","og_site_name":"Comet","article_publisher":"https:\/\/www.facebook.com\/cometdotml","article_published_time":"2026-08-27T14:05:04+00:00","article_modified_time":"2026-08-27T14:05:05+00:00","og_image":[{"width":1800,"height":945,"url":"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2025\/10\/Share-image-3.png","type":"image\/png"}],"author":"Abby Morgan","twitter_card":"summary_large_image","twitter_creator":"@anmorgan2414","twitter_site":"@Cometml","twitter_misc":{"Written by":"Abby Morgan","Est. reading time":"21 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/#article","isPartOf":{"@id":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/"},"author":{"name":"Abby Morgan","@id":"https:\/\/www.comet.com\/site\/#\/schema\/person\/826ee39a2e30cf9d8d73155de09bb7b2"},"headline":"Diffusion Language Models, From Scratch to Production","datePublished":"2026-08-27T14:05:04+00:00","dateModified":"2026-08-27T14:05:05+00:00","mainEntityOfPage":{"@id":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/"},"wordCount":4283,"commentCount":0,"publisher":{"@id":"https:\/\/www.comet.com\/site\/#organization"},"image":{"@id":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/#primaryimage"},"thumbnailUrl":"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/ar_vs_diffusion_static.svg","keywords":["Comet","Diffusion Models","Language Models","LLM","Opik"],"articleSection":["Thought Leadership","Tutorials"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/","url":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/","name":"Diffusion Language Models, From Scratch to Production - Comet","isPartOf":{"@id":"https:\/\/www.comet.com\/site\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/#primaryimage"},"image":{"@id":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/#primaryimage"},"thumbnailUrl":"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/ar_vs_diffusion_static.svg","datePublished":"2026-08-27T14:05:04+00:00","dateModified":"2026-08-27T14:05:05+00:00","description":"Diffusion LLMs trade quality for speed. We build one from scratch and evaluate Mercury 2 against Claude Haiku with Opik to measure exactly when it's worth it","breadcrumb":{"@id":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/#primaryimage","url":"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/ar_vs_diffusion_static.svg","contentUrl":"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/ar_vs_diffusion_static.svg","width":1000,"height":640},{"@type":"BreadcrumbList","@id":"https:\/\/www.comet.com\/site\/blog\/diffusion-language-models\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.comet.com\/site\/"},{"@type":"ListItem","position":2,"name":"Diffusion Language Models, From Scratch to Production"}]},{"@type":"WebSite","@id":"https:\/\/www.comet.com\/site\/#website","url":"https:\/\/www.comet.com\/site\/","name":"Comet","description":"Build Better Models Faster","publisher":{"@id":"https:\/\/www.comet.com\/site\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.comet.com\/site\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.comet.com\/site\/#organization","name":"Comet ML, Inc.","alternateName":"Comet","url":"https:\/\/www.comet.com\/site\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.comet.com\/site\/#\/schema\/logo\/image\/","url":"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2025\/01\/logo_comet_square.png","contentUrl":"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2025\/01\/logo_comet_square.png","width":310,"height":310,"caption":"Comet ML, Inc."},"image":{"@id":"https:\/\/www.comet.com\/site\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/cometdotml","https:\/\/x.com\/Cometml","https:\/\/www.youtube.com\/channel\/UCmN63HKvfXSCS-UwVwmK8Hw"]},{"@type":"Person","@id":"https:\/\/www.comet.com\/site\/#\/schema\/person\/826ee39a2e30cf9d8d73155de09bb7b2","name":"Abby Morgan","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.comet.com\/site\/#\/schema\/person\/image\/dbbf1ae921ee179c768f508340415946","url":"https:\/\/secure.gravatar.com\/avatar\/28d4934d14261b4afe12e226f0eaa57c4fb0c2761ad4586eb9a5bec3b8160bc9?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/28d4934d14261b4afe12e226f0eaa57c4fb0c2761ad4586eb9a5bec3b8160bc9?s=96&d=mm&r=g","caption":"Abby Morgan"},"description":"AI\/ML Growth Engineer @ Comet","sameAs":["https:\/\/www.comet.com\/","https:\/\/www.linkedin.com\/in\/anmorgan24\/","https:\/\/x.com\/anmorgan2414"],"url":"https:\/\/www.comet.com\/site\/blog\/author\/abigailmcomet-com\/"}]}},"jetpack_featured_media_url":"https:\/\/www.comet.com\/site\/wp-content\/uploads\/2026\/08\/ar_vs_diffusion_static.svg","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/posts\/20629","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/users\/22"}],"replies":[{"embeddable":true,"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/comments?post=20629"}],"version-history":[{"count":2,"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/posts\/20629\/revisions"}],"predecessor-version":[{"id":20642,"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/posts\/20629\/revisions\/20642"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/media\/20643"}],"wp:attachment":[{"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/media?parent=20629"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/categories?post=20629"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/tags?post=20629"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.comet.com\/site\/wp-json\/wp\/v2\/coauthors?post=20629"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}