Skip to content

fix(schedulers): skip noise at terminal DDPM step when no predecessor exists - #15142

Open
mirzasnowy wants to merge 1 commit into
huggingface:mainfrom
mirzasnowy:fix/15139-terminal-step-no-noise
Open

mirzasnowy wants to merge 1 commit into
huggingface:mainfrom
mirzasnowy:fix/15139-terminal-step-no-noise

Conversation

@mirzasnowy

Copy link
Copy Markdown

Fixes #15139.

Bug

With variance_type="fixed_large" and timestep_spacing="trailing", DDPMScheduler.step() adds noise to the final clean-data prediction. For 1000 training timesteps and 50 inference steps, the last selected timestep is 19 and previous_timestep(19) returns -1, but step() still enters its noise branch because it checks t > 0. The mean at this boundary is already pred_original_sample (alpha_prod_t_prev = 1), so the fresh random term corrupts the returned clean prediction — and changing only the random seed changes the returned clean endpoint. The same guard exists in DDPMParallelScheduler.step().

Fix

In both step() methods, the noise branch guard changes from t > 0 to prev_t >= 0. prev_t is already computed at the top of each method and already used the same way for the alpha coefficients (alpha_prod_t_prev = ... if prev_t >= 0 else self.one), so the new condition is consistent with the method's own terminal handling. Behavior changes only at terminal steps with t > 0 (trailing spacing or custom schedules ending nonzero); all other steps are bit-identical. The parallel scheduler's step() is not a # Copied from block (verified against the file's copied-block list), so no make fix-copies sync is owed; both guards were changed identically regardless.

Tests

  • Issue repro (CPU, public scheduler API, no model weights): before fix, fixed_large 19 -1 deviates 0.1654895395040512 for both schedulers (fixed_small 0.0); after fix, all four combinations 0.0.
  • New test_terminal_step_adds_no_noise in both test files: FAIL on pristine code, PASS with the fix.
  • Neighbouring suites test_scheduler_ddpm.py + test_scheduler_ddpm_parallel.py: 77 passed. ruff check clean with the repo's own pyproject.toml; ruff format --check clean on the touched files.

Before submitting

  • Did you use an AI agent to help with this PR? If so:
    • Did you read the Coding with AI agents guide?
    • Did you run the self-review skill on the diff?
    • Did you share the final self-review notes in the PR description or a comment?

Self-review notes (final round)

Rubric: .ai/references/review-rules.md (plus code_style copied-code rule).

Blocking issues

None.

Non-blocking issues

  1. Maintainer-preferred formulation (leave for the actual review).
    The issue itself says "This needs maintainer review before proposing a PR."
    This fix follows the reporter's suggested approach: gate noise addition on predecessor existence (prev_t >= 0) instead of on t > 0. That condition is already how the same methods handle the terminal case two lines above (alpha_prod_t_prev = ... if prev_t >= 0 else self.one), so it is the most consistent formulation available. If a maintainer prefers a different formulation (e.g. comparing against the last timestep), the regression tests in this diff still define the required behavior exactly.
  2. Variance-mode coverage (not a gap, noted for completeness).
    The guard change sits above all variance branches (fixed_small, fixed_large, fixed_small_log, learned_range), so it applies uniformly; the reporter reproduced the bug with learned/learned_range as well. The regression tests cover fixed_small/fixed_large (the issue's exact repro). A learned_range case would need a doubled-channel model output to reach the split path; it exercises the same shared guard, so it is not added.

Dead code (advisory)

None added. The change narrows when an existing branch runs; it adds no new code paths, parameters, or classes.

Verdict

READY — fix before submitting: nothing. Leave for the actual review: item 1 above (maintainer-preferred formulation).

I have reviewed every line of this diff.

… exists

With timestep_spacing='trailing', the final selected timestep is nonzero, so DDPMScheduler.step() and DDPMParallelScheduler.step() added fresh noise at the terminal transition even though the mean is already the predicted clean sample. Gate noise addition on predecessor existence (prev_t >= 0) instead of on t > 0, matching how the same methods already handle the terminal case for the alpha coefficients.

Fixes huggingface#15139

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DDPM schedulers add fresh noise on terminal transitions when the final timestep is nonzero

1 participant