Skip to content

Instantly share code, notes, and snippets.

@opparco
Created April 28, 2026 07:49
Show Gist options
  • Select an option

  • Save opparco/76b0c531806cb385bc17c787516c6798 to your computer and use it in GitHub Desktop.

Select an option

Save opparco/76b0c531806cb385bc17c787516c6798 to your computer and use it in GitHub Desktop.
code reading: Automatic Scheduler in stable-diffusion-webui-forge-neo

Automatic Scheduler

How the "Automatic" scheduler sentinel is defined, resolved, and executed.

Definition

"Automatic" is the first entry in the schedulers list (modules/sd_schedulers.py:268), with function=None:

schedulers = [
    Scheduler("automatic", "Automatic", None),   # index 0, function=None
    Scheduler("karras", "Karras", ...),
    ...
]

function=None is the sentinel that tells downstream code to resolve the actual schedule elsewhere.

Resolution at the UI/infotext layer

get_sampler_and_scheduler() (modules/sd_samplers.py:113) parses sampler strings like "Euler a Karras" into ("Euler a", "Karras") and handles the convert_automatic flag (default True):

# if the found scheduler is already the sampler's own built-in default, revert to "Automatic"
if convert_automatic and sampler.options.get("scheduler", None) == found_scheduler.name:
    found_scheduler = sd_schedulers.schedulers[0]  # → Automatic

This keeps the UI and infotext showing "Automatic" rather than the redundant explicit name when the sampler's default is in use. Called with convert_automatic=False when the actual resolved name is needed (e.g. fix_p_invalid_sampler_and_scheduler, line 139).

Resolution at inference time

KDiffusionSampler.get_sigmas() (modules/sd_samplers_kdiffusion.py:80) resolves "Automatic" at runtime:

scheduler_name = (p.hr_scheduler if p.is_hr_pass else p.scheduler) or "Automatic"
if scheduler_name == "Automatic":
    if dynamic_args.klein:
        scheduler_name = "Flux2"      # Klein model special-case
    else:
        scheduler_name = self.config.options.get("scheduler", None)  # sampler's declared default

Resolution priority:

  1. User's explicit scheduler choice
  2. If "Automatic" + Klein model → "Flux2"
  3. If "Automatic" + other → sampler's own options["scheduler"]
  4. If that is also None → fall through to self.model_wrap.get_sigmas(steps) (the model's native schedule)

The "Schedule type" key is intentionally omitted from extra_generation_params when "Automatic" is in effect, so it does not appear in saved infotext unless a non-default scheduler was actually used.

The model's native sigma schedule

When no scheduler function is found (case 4 above), sd_samplers_kdiffusion.py:97 calls:

sigmas = self.model_wrap.get_sigmas(steps)

self.model_wrap is a ForgeScheduleLinker (modules_forge/packages/k_diffusion/external.py:28):

def get_sigmas(self, n=None):
    t_max = len(self.sigmas) - 1                  # e.g. 999 for a 1000-step model
    t = torch.linspace(t_max, 0, n, device=...)   # n evenly-spaced timestep indices
    return append_zero(self.t_to_sigma(t))         # map to sigma values, append 0

This linearly spaces n timestep indices from t_max down to 0, maps them to sigma values via predictor.sigma(t), then appends a trailing zero (the fully-denoised endpoint).

Per-architecture sigma(t) implementations

The predictor is one of the Prediction* classes in backend/modules/k_prediction.py, chosen at model load time:

Class Used by sigma(t)
Prediction SD1.5, SDXL Log-linear interpolation in a log_sigmas table built from the beta schedule: sigmas = sqrt((1 - αbar) / αbar)
PredictionEDM EDM models exp(t / 0.25)
PredictionContinuousEDM EDM continuous exp(t / 0.25) with a log-linear sigma table
PredictionContinuousV V-prediction continuous tan(t · π/2)
PredictionFlow SD3-like time_snr_shift(shift, t / multiplier)
PredictionFlux Flux.1-dev/schnell Identity (sigma == timestep); table pre-shifted by mu via FlowMatchEulerDiscreteScheduler._time_shift_exponential
PredictionFlux2 Flux2-Klein exp(mu) / (exp(mu) + (1/t - 1)^sigma)
PredictionDiscreteFlow Wan, others time_snr_shift(shift, t / multiplier)

SD1.5 concrete example

Prediction.register_schedule builds a 1000-entry sigma table from the linear beta schedule:

betas = make_beta_schedule("linear", 1000, linear_start=0.00085, linear_end=0.012)
alphas_cumprod = cumprod(1 - betas)
sigmas = sqrt((1 - alphas_cumprod) / alphas_cumprod)   # ≈ 0.029 … 14.6

sigma(t) then does log-linear interpolation between adjacent entries so fractional indices get smooth values. Linearly spacing the indices produces a schedule that is uniform in the model's own timestep space — roughly uniform in log-sigma. This is essentially what the named "Uniform" scheduler does explicitly; "Automatic" reads it directly off the model's baked-in table instead of recomputing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment