Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

196 lines
7.4 KiB
Python

"""Tests for AnimaSchedulerDriver — the helper that hides per-scheduler API quirks
(sigmas= vs num_inference_steps=, Heun's doubled timestep array, set_begin_index)
behind a uniform iteration interface."""
import inspect
import pytest
import torch
from invokeai.app.invocations.anima_denoise import loglinear_timestep_shift
from invokeai.backend.anima.scheduler_driver import AnimaSchedulerDriver
from invokeai.backend.flux.schedulers import ANIMA_SCHEDULER_MAP, ANIMA_SHIFT
def _anima_sigmas(num_steps: int) -> list[float]:
return [loglinear_timestep_shift(ANIMA_SHIFT, 1.0 - i / num_steps) for i in range(num_steps + 1)]
@pytest.mark.parametrize("scheduler_name", ["heun", "dpmpp_2m", "dpmpp_2m_sde", "er_sde"])
def test_driver_full_schedule_iteration_count(scheduler_name: str) -> None:
"""For full schedules (no clipping), the driver yields enough iterations to
cover one full denoise. Heun yields 2N-1 iterations for N user steps."""
num_steps = 8
sigmas = _anima_sigmas(num_steps)
driver = AnimaSchedulerDriver(
scheduler_name=scheduler_name,
sigmas=sigmas,
steps=num_steps,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=0,
)
iterations = list(driver.iterations())
if driver.is_heun:
assert len(iterations) == 2 * num_steps - 1
else:
assert len(iterations) == num_steps
@pytest.mark.parametrize("scheduler_name", ["dpmpp_2m", "dpmpp_2m_sde", "er_sde"])
def test_driver_single_step_schedulers_complete_user_step_every_iteration(scheduler_name: str) -> None:
"""Non-Heun schedulers report completes_user_step on every iteration."""
num_steps = 6
driver = AnimaSchedulerDriver(
scheduler_name=scheduler_name,
sigmas=_anima_sigmas(num_steps),
steps=num_steps,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=0,
)
user_step_count = sum(1 for it in driver.iterations() if it.completes_user_step)
assert user_step_count == num_steps
def test_driver_heun_completes_user_step_on_second_order_and_terminal() -> None:
"""Heun yields one completion per user step: each pair's 2nd-order half plus
the unpaired terminal 1st-order step (sigma_prev==0)."""
num_steps = 4
driver = AnimaSchedulerDriver(
scheduler_name="heun",
sigmas=_anima_sigmas(num_steps),
steps=num_steps,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=0,
)
# state_in_first_order only toggles once scheduler.step runs, so drive a fake
# step per iteration to mirror production behaviour.
completes_flags = []
for it in driver.iterations():
completes_flags.append(it.completes_user_step)
driver.scheduler.step(
model_output=torch.zeros((1, 1, 1, 4, 4)),
timestep=it.sched_timestep,
sample=torch.zeros((1, 1, 1, 4, 4)),
)
# N=4 → 7 iterations: indices 1, 3, 5 (SO halves) + 6 (terminal FO) = 4 completions.
assert sum(completes_flags) == num_steps
assert completes_flags[-1] is True, "terminal Heun first-order step must complete its user step"
@pytest.mark.parametrize(
("denoising_start", "denoising_end"),
[(0.0, 1.0), (0.2, 1.0), (0.0, 0.8), (0.2, 0.8), (0.5, 0.75)],
)
def test_driver_dpmpp_clipped_schedule_starts_at_correct_sigma(denoising_start: float, denoising_end: float) -> None:
"""DPM++ doesn't accept sigmas= on diffusers 0.35.1; the driver's set_begin_index
fallback must expose a first iteration whose sigma matches the clipped Anima reference.
DPM++ constructs its internal flow schedule via ``np.linspace(1, T, N+1)[:-1]`` rather
than the closed-form Anima loglinear shift, so the leading-edge sigma is offset by up
to ~2e-3 from the Anima reference. That offset is a property of the scheduler family,
not the driver — same offset exists in the pre-refactor code path.
"""
num_steps = 30
full_sigmas = _anima_sigmas(num_steps)
k_start = int(denoising_start * num_steps)
expected_first_sigma = full_sigmas[k_start]
cls, _ = ANIMA_SCHEDULER_MAP["dpmpp_2m"]
accepts_sigmas = "sigmas" in inspect.signature(cls(num_train_timesteps=1000).set_timesteps).parameters
driver = AnimaSchedulerDriver(
scheduler_name="dpmpp_2m",
sigmas=full_sigmas[k_start : int(denoising_end * num_steps) + 1] if accepts_sigmas else full_sigmas,
steps=num_steps,
denoising_start=denoising_start,
denoising_end=denoising_end,
device=torch.device("cpu"),
seed=0,
)
first_iter = next(driver.iterations())
assert abs(first_iter.sigma_curr - expected_first_sigma) < 2e-3
@pytest.mark.parametrize(
("denoising_start", "denoising_end", "steps"),
[(0.2, 0.8, 30), (0.0, 0.8, 30), (0.2, 1.0, 30), (0.5, 0.75, 20)],
)
def test_driver_heun_clipped_schedule_iteration_count(denoising_start: float, denoising_end: float, steps: int) -> None:
"""Heun clipped schedule: iteration count is 2*(k_end-k_start), clamped so
denoising_end=1.0 doesn't run past the 2N-1 array."""
full_sigmas = _anima_sigmas(steps)
k_start = int(denoising_start * steps)
k_end = int(denoising_end * steps)
driver = AnimaSchedulerDriver(
scheduler_name="heun",
sigmas=full_sigmas,
steps=steps,
denoising_start=denoising_start,
denoising_end=denoising_end,
device=torch.device("cpu"),
seed=0,
)
# If Heun's set_timesteps accepts sigmas=, the driver will pass the full schedule directly
# and yield 2*steps-1 iterations regardless of clipping. The set_begin_index path applies
# only when sigmas= is unsupported.
accepts_sigmas = "sigmas" in inspect.signature(driver.scheduler.set_timesteps).parameters
if accepts_sigmas:
# Driver took the sigma-passing path; sigmas were not pre-clipped here, so the count
# reflects the full schedule.
assert driver.num_iterations == 2 * steps - 1
return
expected = min(2 * (k_end - k_start), len(driver.scheduler.timesteps) - driver.begin_index)
assert driver.num_iterations == expected
assert driver.begin_index == 2 * k_start
def test_driver_terminal_sigma_prev_is_zero() -> None:
"""The last iteration's sigma_prev must be 0.0 (terminal noise level)."""
driver = AnimaSchedulerDriver(
scheduler_name="dpmpp_2m",
sigmas=_anima_sigmas(8),
steps=8,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=0,
)
last_iter = list(driver.iterations())[-1]
assert last_iter.sigma_prev == 0.0
def test_driver_seed_determinism() -> None:
"""Same seed → identical step_generator state → reproducible SDE noise."""
sigmas = _anima_sigmas(8)
driver_a = AnimaSchedulerDriver(
scheduler_name="er_sde",
sigmas=sigmas,
steps=8,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=42,
)
driver_b = AnimaSchedulerDriver(
scheduler_name="er_sde",
sigmas=sigmas,
steps=8,
denoising_start=0.0,
denoising_end=1.0,
device=torch.device("cpu"),
seed=42,
)
# Same seed → same first random draw.
a = torch.randn((1, 4), generator=driver_a.step_generator)
b = torch.randn((1, 4), generator=driver_b.step_generator)
assert torch.equal(a, b)