Skip to content

Fix double timestep shifting in FlowMatchEulerDiscreteScheduler.set_timesteps (#13243)#13706

Open
jbbqqf wants to merge 1 commit intohuggingface:mainfrom
jbbqqf:fix/13243-flow-match-euler-double-shift
Open

Fix double timestep shifting in FlowMatchEulerDiscreteScheduler.set_timesteps (#13243)#13706
jbbqqf wants to merge 1 commit intohuggingface:mainfrom
jbbqqf:fix/13243-flow-match-euler-double-shift

Conversation

@jbbqqf
Copy link
Copy Markdown

@jbbqqf jbbqqf commented May 10, 2026

Fixes #13243.

Summary

FlowMatchEulerDiscreteScheduler.__init__ records self.sigma_max / self.sigma_min after applying the standard `shift * sigmas / (1 + (shift - 1) * sigmas)` transform:

sigmas = timesteps / num_train_timesteps
if not use_dynamic_shifting:
    sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)   # ← shift applied
self.sigmas = sigmas
self.sigma_min = self.sigmas[-1].item()                     # ← post-shift
self.sigma_max = self.sigmas[0].item()                      # ← post-shift

set_timesteps then fed those already-shifted endpoints back into the linspace and applied the same shift again:

timesteps = np.linspace(self._sigma_to_t(self.sigma_max), self._sigma_to_t(self.sigma_min), num_inference_steps)
sigmas = timesteps / self.config.num_train_timesteps
...
sigmas = self.shift * sigmas / (1 + (self.shift - 1) * sigmas)   # ← shift applied again

So calling scheduler.set_timesteps(num_train_timesteps) produced a different schedule than the one __init__ had already computed, for the same (num_train_timesteps, num_inference_steps, shift) triple.

Fix

Use the pre-shift [num_train_timesteps, ..., 1] range directly when no custom timesteps/sigmas are provided. The shift is then applied exactly once below. self.sigma_max/self.sigma_min are left as-is so any consumer reading them externally keeps the same public semantics.

Reproduce BEFORE/AFTER yourself (copy-paste)

# --- BEFORE: origin/main, schedules diverge ---
git fetch origin main && git checkout origin/main
python - <<'PY'
import torch
from diffusers.schedulers.scheduling_flow_match_euler_discrete import FlowMatchEulerDiscreteScheduler
sched = FlowMatchEulerDiscreteScheduler(num_train_timesteps=1000, shift=3.0)
init_sigmas = sched.sigmas.clone()
sched.set_timesteps(1000)
post_sigmas = sched.sigmas.clone()
print('match (excluding terminal 0):', torch.allclose(init_sigmas, post_sigmas[:-1], atol=1e-5))  # Expected: False
print('init head :', init_sigmas[:3].tolist())
print('post head :', post_sigmas[:3].tolist())
PY

# --- AFTER: this branch ---
git fetch origin pull/<PR>/head:fix && git checkout fix
python - <<'PY'
import torch
from diffusers.schedulers.scheduling_flow_match_euler_discrete import FlowMatchEulerDiscreteScheduler
sched = FlowMatchEulerDiscreteScheduler(num_train_timesteps=1000, shift=3.0)
init_sigmas = sched.sigmas.clone()
sched.set_timesteps(1000)
post_sigmas = sched.sigmas.clone()
print('match (excluding terminal 0):', torch.allclose(init_sigmas, post_sigmas[:-1], atol=1e-5))  # Expected: True
PY

I confirmed both runs locally:

  • origin/main → match: False (init head [1.0, 0.9997, 0.9993], set_timesteps head [1.0, 0.9991, 0.9982] — visibly different).
  • this branch → match: True.

Notes

  • Custom timesteps / sigmas paths are unchanged.
  • No public API or attribute changes; self.sigma_min/self.sigma_max keep their current values.

🤖 Disclosure: Authored with assistance from Claude (Anthropic), reproducer run on both refs.

…timesteps` (huggingface#13243)

`__init__` records `self.sigma_max` / `self.sigma_min` *after* applying the
`shift * sigmas / (1 + (shift - 1) * sigmas)` transform. `set_timesteps` then
fed those already-shifted endpoints back into `np.linspace(...)` and applied
the same shift again, producing a different schedule for the same
`(num_train_timesteps, num_inference_steps, shift)` triple than `__init__`.

Use the pre-shift `[num_train_timesteps, ..., 1]` range directly when no
custom timesteps are provided so the shift is applied exactly once. After the
fix, `__init__` and `set_timesteps(num_train_timesteps)` produce identical
sigmas (to atol=1e-5) for every value of `shift` (verified for 1.0 and 3.0).

`self.sigma_max`/`self.sigma_min` are left as-is to preserve their public
attribute semantics for any downstream consumers.
@github-actions github-actions Bot added fixes-issue schedulers size/S PR with diff < 50 LOC and removed fixes-issue labels May 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

schedulers size/S PR with diff < 50 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] FlowMatchEulerDiscreteScheduler.__init__ computes sigma_min/sigma_max after shift, causing duplicate shift in set_timesteps

1 participant