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

250 lines
8.3 KiB
Python

from types import SimpleNamespace
import pytest
import torch
from invokeai.backend.flux.denoise import denoise
from invokeai.backend.flux.schedulers import FLUX_SCHEDULER_MAP
class _FakeFluxModel:
def __call__(
self,
img: torch.Tensor,
img_ids: torch.Tensor,
txt: torch.Tensor,
txt_ids: torch.Tensor,
y: torch.Tensor,
timesteps: torch.Tensor,
guidance: torch.Tensor,
timestep_index: int,
total_num_timesteps: int,
controlnet_double_block_residuals: list[torch.Tensor] | None,
controlnet_single_block_residuals: list[torch.Tensor] | None,
ip_adapter_extensions: list[object],
regional_prompting_extension: object,
) -> torch.Tensor:
return torch.zeros_like(img)
class _FakeDyPEExtension:
def __init__(self) -> None:
self.sigmas: list[float] = []
def patch_model(self, model: object) -> tuple[object, None]:
return object(), None
def update_step_state(self, embedder: object, sigma: float) -> None:
self.sigmas.append(sigma)
class _FakeScheduler:
def __init__(self) -> None:
self.config = SimpleNamespace(num_train_timesteps=1000)
self.timesteps = torch.tensor([], dtype=torch.float32)
self.sigmas = torch.tensor([], dtype=torch.float32)
def set_timesteps(self, sigmas: list[float], device: torch.device) -> None:
del device
self.sigmas = torch.tensor(sigmas, dtype=torch.float32)
self.timesteps = torch.tensor([900.0, 400.0], dtype=torch.float32)
def step(self, model_output: torch.Tensor, timestep: torch.Tensor, sample: torch.Tensor) -> SimpleNamespace:
del model_output, timestep
return SimpleNamespace(prev_sample=sample)
class _FakeHeunScheduler:
def __init__(self) -> None:
self.config = SimpleNamespace(num_train_timesteps=1000)
self.timesteps = torch.tensor([], dtype=torch.float32)
self.sigmas = torch.tensor([], dtype=torch.float32)
self.state_in_first_order = True
self._step_index = 0
def set_timesteps(self, sigmas: list[float], device: torch.device) -> None:
del device
# Duplicate each user-facing step to mimic a second-order scheduler.
self.sigmas = torch.tensor([1.0, 1.0, 0.25, 0.25, 0.0], dtype=torch.float32)
self.timesteps = torch.tensor([900.0, 850.0, 400.0, 350.0], dtype=torch.float32)
self._step_index = 0
self.state_in_first_order = True
def step(self, model_output: torch.Tensor, timestep: torch.Tensor, sample: torch.Tensor) -> SimpleNamespace:
del model_output, timestep
self._step_index += 1
self.state_in_first_order = self._step_index % 2 == 0
return SimpleNamespace(prev_sample=sample)
class _FakePbar:
def update(self, value: int) -> None:
del value
def close(self) -> None:
return None
def _fake_tqdm(iterable=None, **kwargs):
del kwargs
if iterable is None:
return _FakePbar()
return iterable
def _build_regional_prompting_extension(batch_size: int) -> SimpleNamespace:
return SimpleNamespace(
regional_text_conditioning=SimpleNamespace(
t5_embeddings=torch.zeros(batch_size, 1, 4),
t5_txt_ids=torch.zeros(batch_size, 1, 3),
clip_embeddings=torch.zeros(batch_size, 4),
)
)
def test_denoise_euler_path_updates_dype_with_sigma(monkeypatch):
monkeypatch.setattr("invokeai.backend.flux.denoise.tqdm", _fake_tqdm)
model = _FakeFluxModel()
dype_extension = _FakeDyPEExtension()
img = torch.zeros(1, 2, 4)
img_ids = torch.zeros(1, 2, 3)
regional_prompting_extension = _build_regional_prompting_extension(batch_size=1)
callback_steps: list[int] = []
result = denoise(
model=model,
img=img,
img_ids=img_ids,
pos_regional_prompting_extension=regional_prompting_extension,
neg_regional_prompting_extension=None,
timesteps=[1.0, 0.5, 0.0],
step_callback=lambda state: callback_steps.append(state.step),
guidance=1.0,
cfg_scale=[1.0, 1.0],
inpaint_extension=None,
controlnet_extensions=[],
pos_ip_adapter_extensions=[],
neg_ip_adapter_extensions=[],
img_cond=None,
img_cond_seq=None,
img_cond_seq_ids=None,
dype_extension=dype_extension,
scheduler=None,
)
assert torch.equal(result, img)
assert dype_extension.sigmas == [1.0, 0.5]
assert callback_steps == [1, 2]
def test_denoise_scheduler_path_prefers_scheduler_sigmas_for_dype(monkeypatch):
monkeypatch.setattr("invokeai.backend.flux.denoise.tqdm", _fake_tqdm)
model = _FakeFluxModel()
scheduler = _FakeScheduler()
dype_extension = _FakeDyPEExtension()
img = torch.zeros(1, 2, 4)
img_ids = torch.zeros(1, 2, 3)
regional_prompting_extension = _build_regional_prompting_extension(batch_size=1)
denoise(
model=model,
img=img,
img_ids=img_ids,
pos_regional_prompting_extension=regional_prompting_extension,
neg_regional_prompting_extension=None,
timesteps=[1.0, 0.25, 0.0],
step_callback=lambda state: None,
guidance=1.0,
cfg_scale=[1.0, 1.0],
inpaint_extension=None,
controlnet_extensions=[],
pos_ip_adapter_extensions=[],
neg_ip_adapter_extensions=[],
img_cond=None,
img_cond_seq=None,
img_cond_seq_ids=None,
dype_extension=dype_extension,
scheduler=scheduler,
)
# Scheduler timesteps normalize to [0.9, 0.4], so this asserts the scheduler
# sigma sequence is what DyPE actually consumes.
assert dype_extension.sigmas == [1.0, 0.25]
def test_denoise_heun_scheduler_path_uses_internal_scheduler_sigmas(monkeypatch):
monkeypatch.setattr("invokeai.backend.flux.denoise.tqdm", _fake_tqdm)
model = _FakeFluxModel()
scheduler = _FakeHeunScheduler()
dype_extension = _FakeDyPEExtension()
img = torch.zeros(1, 2, 4)
img_ids = torch.zeros(1, 2, 3)
regional_prompting_extension = _build_regional_prompting_extension(batch_size=1)
callback_steps: list[int] = []
denoise(
model=model,
img=img,
img_ids=img_ids,
pos_regional_prompting_extension=regional_prompting_extension,
neg_regional_prompting_extension=None,
timesteps=[1.0, 0.25, 0.0],
step_callback=lambda state: callback_steps.append(state.step),
guidance=1.0,
cfg_scale=[1.0, 1.0],
inpaint_extension=None,
controlnet_extensions=[],
pos_ip_adapter_extensions=[],
neg_ip_adapter_extensions=[],
img_cond=None,
img_cond_seq=None,
img_cond_seq_ids=None,
dype_extension=dype_extension,
scheduler=scheduler,
)
assert dype_extension.sigmas == [1.0, 1.0, 0.25, 0.25]
assert callback_steps == [1, 2]
@pytest.mark.parametrize("scheduler_name", sorted(FLUX_SCHEDULER_MAP))
def test_denoise_real_flux_schedulers_update_dype_from_internal_sigma_schedule(monkeypatch, scheduler_name):
monkeypatch.setattr("invokeai.backend.flux.denoise.tqdm", _fake_tqdm)
model = _FakeFluxModel()
scheduler = FLUX_SCHEDULER_MAP[scheduler_name](num_train_timesteps=1000)
dype_extension = _FakeDyPEExtension()
img = torch.zeros(1, 2, 4)
img_ids = torch.zeros(1, 2, 3)
regional_prompting_extension = _build_regional_prompting_extension(batch_size=1)
callback_steps: list[int] = []
denoise(
model=model,
img=img,
img_ids=img_ids,
pos_regional_prompting_extension=regional_prompting_extension,
neg_regional_prompting_extension=None,
timesteps=[1.0, 0.25, 0.0],
step_callback=lambda state: callback_steps.append(state.step),
guidance=1.0,
cfg_scale=[1.0, 1.0],
inpaint_extension=None,
controlnet_extensions=[],
pos_ip_adapter_extensions=[],
neg_ip_adapter_extensions=[],
img_cond=None,
img_cond_seq=None,
img_cond_seq_ids=None,
dype_extension=dype_extension,
scheduler=scheduler,
)
assert dype_extension.sigmas
expected_sigmas = [float(sigma) for sigma in scheduler.sigmas[: len(dype_extension.sigmas)]]
assert dype_extension.sigmas == expected_sigmas
assert callback_steps