diff --git a/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py b/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py index 0e4f5c6a1f97..a181fcd82071 100644 --- a/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py +++ b/src/diffusers/schedulers/scheduling_flow_match_euler_discrete.py @@ -350,8 +350,11 @@ 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 (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) # 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..cc3b914661e1 100644 --- a/src/diffusers/schedulers/scheduling_flow_match_lcm.py +++ b/src/diffusers/schedulers/scheduling_flow_match_lcm.py @@ -359,8 +359,11 @@ 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 (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 # 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: 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())