Skip to content

LTXEulerAncestralRFScheduler.set_timesteps(sigmas=...) does not validate monotonicity, causing silent incorrect denoising #13411

@VittoriaLanzo

Description

@VittoriaLanzo

Describe the bug

LTXEulerAncestralRFScheduler.set_timesteps accepts an externally-supplied sigmas argument without validating that the schedule is monotonically non-increasing. When step() is called on a non-monotone schedule, the ancestral RF decomposition computes sigma_down outside [0, 1] and alpha_down < 0, which violates the CONST parametrization invariant. The output is numerically finite — no exception, no NaN, no warning — but denoising does not occur. The corruption is silent.

From PHILOSOPHY.md, line 32:

"Raising concise error messages is preferred to silently correct erroneous input. Diffusers aims at teaching the user, rather than making the library as easy to use as possible."

Reproduction

import torch
from diffusers import LTXEulerAncestralRFScheduler

scheduler = LTXEulerAncestralRFScheduler()

# Non-monotone schedule: sigma increases at step 0 → 1.
# A misconfigured ComfyUI workflow can emit exactly this kind of list.
scheduler.set_timesteps(sigmas=[0.2, 0.8, 0.5, 0.0])  # should raise — does not

torch.manual_seed(0)
sample       = torch.randn(1, 4, 8, 8)
model_output = torch.zeros_like(sample)  # model predicts zero signal

out = scheduler.step(model_output, scheduler.timesteps[0], sample)

print(f"Input  norm : {sample.norm():.4f}")
print(f"Output norm : {out.prev_sample.norm():.4f}")
print(f"Ratio       : {(out.prev_sample.norm() / sample.norm()).item():.3f}x")
# Expected: small positive ratio converging toward sigma_next/sigma ≈ 0.25
# Actual:   ratio ≈ 1.45, sign-inverted — latent is corrupted, not denoised

Expected behaviour

set_timesteps raises ValueError immediately when sigmas is not monotonically non-increasing, before any internal state is mutated.

Actual behaviour — full numerical trace

With sigma = 0.2, sigma_next = 0.8, eta = 1.0 (from step() in scheduling_ltx_euler_ancestral_rf.py):

downstep_ratio = 1.0 + (sigma_next / sigma - 1.0) * eta
               = 1.0 + (0.8 / 0.2 - 1.0) * 1.0
               = 4.0          ← should be in (0, 1] for a valid denoising step

sigma_down     = sigma_next * downstep_ratio
               = 0.8 * 4.0 = 3.2     ← outside [0, 1]; CONST parametrization undefined

sigma_ratio    = sigma_down / sigma = 3.2 / 0.2 = 16.0

x_euler = 16.0 * sample + (−15.0) * denoised    ← 16× amplification

alpha_down = 1 − sigma_down = 1 − 3.2 = −2.2    ← negative; undefined in CONST space
scale      = alpha_ip1 / alpha_down = 0.2 / −2.2 = −0.091  ← sign flip

# renoise_coeff = sqrt(clamp(sigma_next² − sigma_down² · alpha_ip1² / alpha_down², min=0))
renoise_coeff ≈ 0.745

prev_sample ≈ −0.091 × x_euler + 0.745 × noise
            ≈ −1.45 × sample + 1.36 × denoised + 0.745 × noise

The sample coefficient is −1.45 (sign-inverted, slightly amplified) rather than a small positive value. The output is finite, plausibly shaped, and completely wrong.

Why >= not >

The check should use sigmas[:-1] >= sigmas[1:] (non-strict). Strict monotonicity would reject plateau steps — consecutive equal sigmas that are intentional in img2img partial schedules using set_begin_index. Non-strict preserves that use case while catching every reversed or partially-reversed schedule.

Proposed fix

# set_timesteps(), after the ndim guard, before the terminal-sigma warning:
if len(sigmas_tensor) > 1 and not (sigmas_tensor[:-1] >= sigmas_tensor[1:]).all():
    sig_list = sigmas_tensor.tolist()
    sig_repr = str(sig_list) if len(sig_list) <= 8 else f"{sig_list[:4]} ... {sig_list[-4:]} (len={len(sig_list)})"
    raise ValueError(
        f"`sigmas` must be monotonically non-increasing (each entry >= the next), got {sig_repr}"
    )

A companion range check is also needed — the CONST parametrization requires σ ∈ [0, 1], which set_timesteps does not currently enforce.

Note: FlowMatchEulerDiscreteScheduler is not affected — it generates its sigma schedule internally and does not expose a raw sigmas= argument. The blast radius is asymmetric.

Additional context

There is currently no test file for LTXEulerAncestralRFScheduler in tests/schedulers/, and the scheduler is absent from the API reference _toctree.yml despite being publicly exported.

I have a PR ready that addresses all of the above: monotonicity and range checks, eta boundary validation, 16 unit tests, an API docs page, and the toctree entry. Happy to open it once this issue is acknowledged.

/cc @yiyixuxu

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions