From fcd6ee3c90f8be360f71b28e06c29a3d5717c7dc Mon Sep 17 00:00:00 2001 From: Mayuri Date: Fri, 7 Aug 2026 13:58:19 +0530 Subject: [PATCH 1/2] Fix division-by-zero NaN in shift_terminal with a single denoising step 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 #14411 --- .../schedulers/scheduling_flow_match_euler_discrete.py | 6 ++++-- src/diffusers/schedulers/scheduling_flow_match_lcm.py | 6 ++++-- src/diffusers/schedulers/scheduling_unipc_multistep.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py b/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py index 0e4f5c6a1f97..b034d7d2ecb6 100644 --- a/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py +++ b/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py @@ -350,8 +350,10 @@ def set_timesteps( else: sigmas = self.shift * sigmas / (1 + (self.shift - 1) * sigmas) - # 3. If required, stretch the sigmas schedule to terminate at the configured `shift_terminal` value - if self.config.shift_terminal: + # 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) # 4. If required, convert sigmas to one of karras, exponential, or beta sigma schedules diff --git a/src/diffusers/schedulers/scheduling_flow_match_lcm.py b/src/diffusers/schedulers/scheduling_flow_match_lcm.py index 97d4ebbc8e42..9b9a53efc7ac 100644 --- a/src/diffusers/schedulers/scheduling_flow_match_lcm.py +++ b/src/diffusers/schedulers/scheduling_flow_match_lcm.py @@ -359,8 +359,10 @@ def set_timesteps( else: sigmas = self.shift * sigmas / (1 + (self.shift - 1) * sigmas) # type: ignore - # 3. If required, stretch the sigmas schedule to terminate at the configured `shift_terminal` value - if self.config.shift_terminal: + # 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) # type: ignore # 4. If required, convert sigmas to one of karras, exponential, or beta sigma schedules diff --git a/src/diffusers/schedulers/scheduling_unipc_multistep.py b/src/diffusers/schedulers/scheduling_unipc_multistep.py index 5c2cbcc13ff1..74b4147a4375 100644 --- a/src/diffusers/schedulers/scheduling_unipc_multistep.py +++ b/src/diffusers/schedulers/scheduling_unipc_multistep.py @@ -432,7 +432,7 @@ def set_timesteps( sigmas = self.time_shift(mu, 1.0, sigmas) else: sigmas = self.config.flow_shift * sigmas / (1 + (self.config.flow_shift - 1) * sigmas) - if self.config.shift_terminal: + if self.config.shift_terminal and len(sigmas) > 1: sigmas = self.stretch_shift_to_terminal(sigmas) eps = 1e-6 if np.fabs(sigmas[0] - 1) < eps: From 1e09082b2d2dc2e47b1db17a6fb2b3e2f148ca3b Mon Sep 17 00:00:00 2001 From: Mayuri Date: Sat, 8 Aug 2026 12:14:48 +0530 Subject: [PATCH 2/2] Add regression test for shift_terminal single-step fix, clarify comment Addresses review feedback on #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. --- .../scheduling_flow_match_euler_discrete.py | 3 +- .../schedulers/scheduling_flow_match_lcm.py | 3 +- ...st_scheduler_shift_terminal_single_step.py | 45 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/schedulers/test_scheduler_shift_terminal_single_step.py diff --git a/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py b/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py index b034d7d2ecb6..a181fcd82071 100644 --- a/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py +++ b/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py @@ -352,7 +352,8 @@ def set_timesteps( # 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). + # otherwise divides by zero (with the default schedule the single sigma is always 1.0, so + # `one_minus_z[-1]` is always 0). if self.config.shift_terminal and len(sigmas) > 1: sigmas = self.stretch_shift_to_terminal(sigmas) diff --git a/src/diffusers/schedulers/scheduling_flow_match_lcm.py b/src/diffusers/schedulers/scheduling_flow_match_lcm.py index 9b9a53efc7ac..cc3b914661e1 100644 --- a/src/diffusers/schedulers/scheduling_flow_match_lcm.py +++ b/src/diffusers/schedulers/scheduling_flow_match_lcm.py @@ -361,7 +361,8 @@ def set_timesteps( # 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). + # otherwise divides by zero (with the default schedule the single sigma is always 1.0, so + # `one_minus_z[-1]` is always 0). if self.config.shift_terminal and len(sigmas) > 1: sigmas = self.stretch_shift_to_terminal(sigmas) # type: ignore diff --git a/tests/schedulers/test_scheduler_shift_terminal_single_step.py b/tests/schedulers/test_scheduler_shift_terminal_single_step.py new file mode 100644 index 000000000000..6c9997013bd0 --- /dev/null +++ b/tests/schedulers/test_scheduler_shift_terminal_single_step.py @@ -0,0 +1,45 @@ +# Copyright 2026 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import torch + +from diffusers import FlowMatchEulerDiscreteScheduler, FlowMatchLCMScheduler, UniPCMultistepScheduler + + +class ShiftTerminalSingleStepTest(unittest.TestCase): + """ + Regression test for https://github.com/huggingface/diffusers/issues/14411. + + `stretch_shift_to_terminal()` rescales sigmas by `one_minus_z[-1] / (1 - shift_terminal)`. With + `num_inference_steps=1` the only sigma is 1.0, so `one_minus_z[-1]` is 0 and the rescale divides by + zero, producing a NaN sigma. Schedulers that support `shift_terminal` must skip the stretch when + there is only a single step instead of stretching into NaN. + """ + + def test_flow_match_euler_discrete_single_step_no_nan(self): + scheduler = FlowMatchEulerDiscreteScheduler(shift_terminal=0.1) + scheduler.set_timesteps(num_inference_steps=1) + self.assertFalse(torch.isnan(scheduler.sigmas).any()) + + def test_flow_match_lcm_single_step_no_nan(self): + scheduler = FlowMatchLCMScheduler(shift_terminal=0.1) + scheduler.set_timesteps(num_inference_steps=1) + self.assertFalse(torch.isnan(scheduler.sigmas).any()) + + def test_unipc_flow_sigmas_single_step_no_nan(self): + scheduler = UniPCMultistepScheduler(use_flow_sigmas=True, shift_terminal=0.1) + scheduler.set_timesteps(num_inference_steps=1) + self.assertFalse(torch.isnan(scheduler.sigmas).any())