Fix division-by-zero NaN in shift_terminal with a single denoising step - #14415
Fix division-by-zero NaN in shift_terminal with a single denoising step#14415mayuriphad wants to merge 2 commits into
Conversation
stretch_shift_to_terminal() rescales sigmas so the schedule ends at config.shift_terminal, using scale_factor = one_minus_z[-1] / (1 - shift_terminal). With num_inference_steps=1 the only sigma is always 1.0, so one_minus_z[-1] is 0, scale_factor is 0, and the division produces NaN. That NaN sigma then breaks index_for_timestep() with an IndexError during scheduler.step(). There is nothing to stretch with a single step, so skip the call when len(sigmas) <= 1. Applied the same guard to the three schedulers that support shift_terminal: FlowMatchEulerDiscreteScheduler, FlowMatchLCMScheduler, and UniPCMultistepScheduler (flow-sigmas path). Fixes huggingface#14411
There was a problem hiding this comment.
Pull request overview
Fixes a division-by-zero NaN in stretch_shift_to_terminal() when num_inference_steps=1 and shift_terminal is enabled, by skipping the terminal-stretch rescaling when there’s only a single sigma value. This prevents NaN sigmas from propagating into index_for_timestep() / scheduler.step().
Changes:
- Guard
stretch_shift_to_terminal()application withlen(sigmas) > 1in all schedulers that supportshift_terminal. - Add/adjust inline rationale comments explaining why the single-step case is skipped (FlowMatch schedulers).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py | Skip terminal stretch when only one sigma to prevent NaN/div-by-zero in single-step inference. |
| src/diffusers/schedulers/scheduling_flow_match_lcm.py | Apply the same single-step guard for shift_terminal stretching (LCM variant). |
| src/diffusers/schedulers/scheduling_unipc_multistep.py | Apply the single-step guard on the UniPC flow-sigmas path to avoid the same NaN edge case. |
Suppressed comments (1)
src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py:355
- The explanatory comment here is slightly too absolute:
set_timestepsallows customsigmas/timesteps, so withnum_inference_steps=1the single sigma is not necessarily1.0. The division-by-zero rationale applies to the default schedule (and typical usage), so it would be clearer to qualify the statement to avoid misleading future readers.
# 3. If required, stretch the sigmas schedule to terminate at the configured `shift_terminal` value. This is
# skipped when there is only a single step, since there is nothing to stretch and the terminal rescaling
# otherwise divides by zero (the single sigma is always 1.0, i.e. `one_minus_z[-1]` is always 0).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # 3. If required, stretch the sigmas schedule to terminate at the configured `shift_terminal` value. This is | ||
| # skipped when there is only a single step, since there is nothing to stretch and the terminal rescaling | ||
| # otherwise divides by zero (the single sigma is always 1.0, i.e. `one_minus_z[-1]` is always 0). | ||
| if self.config.shift_terminal and len(sigmas) > 1: | ||
| sigmas = self.stretch_shift_to_terminal(sigmas) |
| # 3. If required, stretch the sigmas schedule to terminate at the configured `shift_terminal` value. This is | ||
| # skipped when there is only a single step, since there is nothing to stretch and the terminal rescaling | ||
| # otherwise divides by zero (the single sigma is always 1.0, i.e. `one_minus_z[-1]` is always 0). |
Addresses review feedback on huggingface#14415: adds a dedicated test covering FlowMatchEulerDiscreteScheduler, FlowMatchLCMScheduler, and UniPCMultistepScheduler (flow-sigmas) with num_inference_steps=1 and shift_terminal set, asserting no NaN sigmas. Also qualifies the comment in both flow-match schedulers: the "single sigma is always 1.0" claim only holds for the default schedule, since set_timesteps also accepts custom sigmas/timesteps.
Addresses review feedback on huggingface#14415: adds a dedicated test covering FlowMatchEulerDiscreteScheduler, FlowMatchLCMScheduler, and UniPCMultistepScheduler (flow-sigmas) with num_inference_steps=1 and shift_terminal set, asserting no NaN sigmas. Also qualifies the comment in both flow-match schedulers: the "single sigma is always 1.0" claim only holds for the default schedule, since set_timesteps also accepts custom sigmas/timesteps. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
c99bd6e to
1e09082
Compare
|
Independent verification on a clean checkout, in case it saves review time. Setup: macOS arm64, CPU only, Python 3.14, torch 2.13.0. The regression test guards. With only No behaviour change for n ≥ 2. I dumped Test suite unchanged. One gap in the guard. from diffusers import FlowMatchEulerDiscreteScheduler
s = FlowMatchEulerDiscreteScheduler(shift_terminal=0.1)
s.set_timesteps(sigmas=[0.9, 1.0])
print(s.sigmas) # tensor([-inf, nan, 0.])
scale_factor = one_minus_z[-1] / (1 - self.config.shift_terminal)
if scale_factor == 0:
return tI ran that variant as well: Also worth knowing: on the UniPC path the failure is silent rather than loud. Limits of this check: CPU only, no pipeline-level run with real weights, no GPU or MPS. Disclosure: this comment comes from an automated agent account, not a human contributor. Every number above was produced by running the code locally and is reproducible from the versions named. |
Fixes #14411
Root cause
stretch_shift_to_terminal()rescales the sigma schedule so it terminates atconfig.shift_terminal, computingscale_factor = one_minus_z[-1] / (1 - shift_terminal). Whennum_inference_steps=1, the single sigma is always1.0, soone_minus_z[-1] == 0, makingscale_factor == 0and the division produceNaN. ThatNaNsigma then crashesindex_for_timestep()with anIndexErrorinsidescheduler.step().Fix
There is nothing to stretch with only one step, so skip the call to
stretch_shift_to_terminal()whenlen(sigmas) <= 1. The sameif self.config.shift_terminal:pattern (guarding a call tostretch_shift_to_terminal) exists in three schedulers that supportshift_terminal, so the guard is applied consistently to all three:FlowMatchEulerDiscreteSchedulerFlowMatchLCMSchedulerUniPCMultistepScheduler(flow-sigmas path)None of the touched lines are inside
# Copied fromblocks, so nomake fix-copiesfollow-up is needed.Verification
FlowMatchEulerDiscreteScheduler.set_timesteps(num_inference_steps=1, ...)withshift_terminal=0.1against the patched code: sigmas are[1., 0.], no NaN.num_inference_steps=4is unaffected and still correctly terminates atshift_terminal=0.1([1.0, 0.7, 0.4, 0.1, 0.0]).