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
500 lines
15 KiB
Python
500 lines
15 KiB
Python
"""Tests for DyPE (Dynamic Position Extrapolation) module."""
|
|
|
|
import torch
|
|
|
|
from invokeai.backend.flux.dype.base import (
|
|
DyPEConfig,
|
|
compute_vision_yarn_freqs,
|
|
get_timestep_kappa,
|
|
)
|
|
from invokeai.backend.flux.dype.embed import DyPEEmbedND
|
|
from invokeai.backend.flux.dype.presets import (
|
|
DYPE_PRESET_4K,
|
|
DYPE_PRESET_AREA,
|
|
DYPE_PRESET_AUTO,
|
|
DYPE_PRESET_MANUAL,
|
|
DYPE_PRESET_OFF,
|
|
DYPE_PRESETS,
|
|
get_dype_config_for_area,
|
|
get_dype_config_for_resolution,
|
|
get_dype_config_from_preset,
|
|
)
|
|
from invokeai.backend.flux.dype.rope import rope_dype
|
|
from invokeai.backend.flux.extensions.dype_extension import DyPEExtension
|
|
|
|
|
|
class TestDyPEConfig:
|
|
"""Tests for DyPEConfig dataclass."""
|
|
|
|
def test_default_values(self):
|
|
config = DyPEConfig()
|
|
assert config.enable_dype is True
|
|
assert config.base_resolution == 1024
|
|
assert config.dype_scale == 2.0
|
|
assert config.dype_exponent == 2.0
|
|
assert config.dype_start_sigma == 1.0
|
|
|
|
def test_custom_values(self):
|
|
config = DyPEConfig(
|
|
enable_dype=False,
|
|
base_resolution=512,
|
|
dype_scale=4.0,
|
|
dype_exponent=3.0,
|
|
dype_start_sigma=0.5,
|
|
)
|
|
assert config.enable_dype is False
|
|
assert config.base_resolution == 512
|
|
assert config.dype_scale == 4.0
|
|
|
|
|
|
class TestDyPEExtension:
|
|
"""Tests for DyPE extension helpers."""
|
|
|
|
def test_resolve_step_sigma_prefers_scheduler_sigmas_tensor(self):
|
|
sigma = DyPEExtension.resolve_step_sigma(
|
|
fallback_sigma=0.42,
|
|
step_index=1,
|
|
scheduler_sigmas=torch.tensor([1.0, 0.75, 0.5]),
|
|
)
|
|
assert sigma == 0.75
|
|
|
|
def test_resolve_step_sigma_falls_back_without_scheduler_sigmas(self):
|
|
sigma = DyPEExtension.resolve_step_sigma(
|
|
fallback_sigma=0.42,
|
|
step_index=1,
|
|
scheduler_sigmas=None,
|
|
)
|
|
assert sigma == 0.42
|
|
|
|
|
|
class TestKappa:
|
|
"""Tests for the DyPE timestep scheduler."""
|
|
|
|
def test_get_timestep_kappa_clamps_to_zero_without_scale(self):
|
|
assert (
|
|
get_timestep_kappa(
|
|
current_sigma=0.5,
|
|
dype_scale=0.0,
|
|
dype_exponent=2.0,
|
|
dype_start_sigma=1.0,
|
|
)
|
|
== 0.0
|
|
)
|
|
|
|
def test_get_timestep_kappa_is_stronger_early(self):
|
|
early_kappa = get_timestep_kappa(
|
|
current_sigma=1.0,
|
|
dype_scale=2.0,
|
|
dype_exponent=2.0,
|
|
dype_start_sigma=1.0,
|
|
)
|
|
late_kappa = get_timestep_kappa(
|
|
current_sigma=0.1,
|
|
dype_scale=2.0,
|
|
dype_exponent=2.0,
|
|
dype_start_sigma=1.0,
|
|
)
|
|
|
|
assert early_kappa == 2.0
|
|
assert late_kappa < early_kappa
|
|
|
|
def test_get_timestep_kappa_clamps_above_start_sigma(self):
|
|
kappa = get_timestep_kappa(
|
|
current_sigma=2.0,
|
|
dype_scale=2.0,
|
|
dype_exponent=2.0,
|
|
dype_start_sigma=1.0,
|
|
)
|
|
assert kappa == 2.0
|
|
|
|
|
|
class TestRopeDype:
|
|
"""Tests for DyPE-enhanced RoPE function."""
|
|
|
|
def test_rope_dype_shape(self):
|
|
"""Test that rope_dype returns correct shape."""
|
|
pos = torch.zeros(1, 64)
|
|
dim = 64
|
|
theta = 10000
|
|
|
|
config = DyPEConfig()
|
|
result = rope_dype(
|
|
pos=pos,
|
|
dim=dim,
|
|
theta=theta,
|
|
current_sigma=0.5,
|
|
target_height=2048,
|
|
target_width=2048,
|
|
dype_config=config,
|
|
)
|
|
|
|
# Shape should be (batch, seq_len, dim/2, 2, 2)
|
|
assert result.shape == (1, 64, dim // 2, 2, 2)
|
|
|
|
def test_rope_dype_no_scaling(self):
|
|
"""When target is same as base, output should match base rope."""
|
|
pos = torch.arange(16).unsqueeze(0).float()
|
|
dim = 32
|
|
theta = 10000
|
|
|
|
config = DyPEConfig(base_resolution=1024)
|
|
|
|
# No scaling needed
|
|
result_no_scale = rope_dype(
|
|
pos=pos,
|
|
dim=dim,
|
|
theta=theta,
|
|
current_sigma=0.5,
|
|
target_height=1024,
|
|
target_width=1024,
|
|
dype_config=config,
|
|
)
|
|
|
|
# With scaling
|
|
result_with_scale = rope_dype(
|
|
pos=pos,
|
|
dim=dim,
|
|
theta=theta,
|
|
current_sigma=0.5,
|
|
target_height=2048,
|
|
target_width=2048,
|
|
dype_config=config,
|
|
)
|
|
|
|
# Results should be different when scaling is applied
|
|
assert not torch.allclose(result_no_scale, result_with_scale)
|
|
|
|
def test_rope_dype_late_stage_moves_toward_base_rope(self):
|
|
"""Late-stage DyPE should be closer to base RoPE than early-stage DyPE."""
|
|
pos = torch.arange(16).unsqueeze(0).float()
|
|
dim = 32
|
|
theta = 10000
|
|
|
|
config = DyPEConfig(base_resolution=1024)
|
|
|
|
base_result = rope_dype(
|
|
pos=pos,
|
|
dim=dim,
|
|
theta=theta,
|
|
current_sigma=1.0,
|
|
target_height=1024,
|
|
target_width=1024,
|
|
dype_config=config,
|
|
)
|
|
early_result = rope_dype(
|
|
pos=pos,
|
|
dim=dim,
|
|
theta=theta,
|
|
current_sigma=1.0,
|
|
target_height=2048,
|
|
target_width=2048,
|
|
dype_config=config,
|
|
)
|
|
late_result = rope_dype(
|
|
pos=pos,
|
|
dim=dim,
|
|
theta=theta,
|
|
current_sigma=0.05,
|
|
target_height=2048,
|
|
target_width=2048,
|
|
dype_config=config,
|
|
)
|
|
|
|
early_delta = torch.mean(torch.abs(early_result - base_result))
|
|
late_delta = torch.mean(torch.abs(late_result - base_result))
|
|
|
|
assert late_delta < early_delta
|
|
|
|
|
|
class TestDyPEEmbedND:
|
|
"""Tests for DyPEEmbedND module."""
|
|
|
|
def test_init(self):
|
|
"""Test DyPEEmbedND initialization."""
|
|
config = DyPEConfig()
|
|
embedder = DyPEEmbedND(
|
|
dim=128,
|
|
theta=10000,
|
|
axes_dim=[16, 56, 56],
|
|
dype_config=config,
|
|
)
|
|
|
|
assert embedder.dim == 128
|
|
assert embedder.theta == 10000
|
|
assert embedder.axes_dim == [16, 56, 56]
|
|
|
|
def test_set_step_state(self):
|
|
"""Test step state update."""
|
|
config = DyPEConfig()
|
|
embedder = DyPEEmbedND(
|
|
dim=128,
|
|
theta=10000,
|
|
axes_dim=[16, 56, 56],
|
|
dype_config=config,
|
|
)
|
|
|
|
embedder.set_step_state(sigma=0.5, height=2048, width=2048)
|
|
|
|
assert embedder._current_sigma == 0.5
|
|
assert embedder._target_height == 2048
|
|
assert embedder._target_width == 2048
|
|
|
|
def test_forward_shape(self):
|
|
"""Test forward pass output shape."""
|
|
config = DyPEConfig()
|
|
embedder = DyPEEmbedND(
|
|
dim=128,
|
|
theta=10000,
|
|
axes_dim=[16, 56, 56],
|
|
dype_config=config,
|
|
)
|
|
|
|
# Create input ids tensor (batch=1, seq_len=64, n_axes=3)
|
|
ids = torch.zeros(1, 64, 3)
|
|
|
|
result = embedder(ids)
|
|
|
|
# Output should have shape (batch, 1, seq_len, dim)
|
|
# Actually the shape is (batch, 1, seq_len, dim/2, 2, 2) based on rope output
|
|
assert result.dim() == 6
|
|
assert result.shape[0] == 1 # batch
|
|
assert result.shape[1] == 1 # unsqueeze
|
|
assert result.shape[2] == 64 # seq_len
|
|
|
|
|
|
class TestDyPEPresets:
|
|
"""Tests for DyPE preset configurations."""
|
|
|
|
def test_preset_4k_exists(self):
|
|
"""Test that 4K preset is defined."""
|
|
assert DYPE_PRESET_4K in DYPE_PRESETS
|
|
|
|
def test_get_dype_config_for_resolution_below_threshold(self):
|
|
"""When resolution is below threshold, should return None."""
|
|
config = get_dype_config_for_resolution(
|
|
width=1024,
|
|
height=1024,
|
|
activation_threshold=1536,
|
|
)
|
|
assert config is None
|
|
|
|
config = get_dype_config_for_resolution(
|
|
width=1536,
|
|
height=1024,
|
|
activation_threshold=1536,
|
|
)
|
|
assert config is None
|
|
|
|
def test_get_dype_config_for_resolution_above_threshold(self):
|
|
"""When resolution is above threshold, should return config."""
|
|
config = get_dype_config_for_resolution(
|
|
width=2048,
|
|
height=2048,
|
|
activation_threshold=1536,
|
|
)
|
|
assert config is not None
|
|
assert config.enable_dype is True
|
|
|
|
def test_get_dype_config_for_resolution_dynamic_scale(self):
|
|
"""Higher resolution should result in higher dype_scale."""
|
|
config_2k = get_dype_config_for_resolution(
|
|
width=2048,
|
|
height=2048,
|
|
base_resolution=1024,
|
|
activation_threshold=1536,
|
|
)
|
|
config_4k = get_dype_config_for_resolution(
|
|
width=4096,
|
|
height=4096,
|
|
base_resolution=1024,
|
|
activation_threshold=1536,
|
|
)
|
|
|
|
assert config_2k is not None
|
|
assert config_4k is not None
|
|
assert config_4k.dype_scale > config_2k.dype_scale
|
|
|
|
def test_get_dype_config_for_area_below_threshold(self):
|
|
"""When area is below threshold area, should return None."""
|
|
config = get_dype_config_for_area(
|
|
width=1024,
|
|
height=1024,
|
|
)
|
|
assert config is None
|
|
|
|
def test_get_dype_config_for_area_above_threshold(self):
|
|
"""When area is above threshold area, should return config."""
|
|
config = get_dype_config_for_area(
|
|
width=2048,
|
|
height=1536,
|
|
base_resolution=1024,
|
|
)
|
|
assert config is not None
|
|
assert config.enable_dype is True
|
|
|
|
def test_get_dype_config_for_area_penalizes_extreme_aspect_ratios(self):
|
|
balanced_extreme = get_dype_config_for_area(
|
|
width=2304,
|
|
height=1152,
|
|
base_resolution=1024,
|
|
)
|
|
extreme = get_dype_config_for_area(
|
|
width=2304,
|
|
height=960,
|
|
base_resolution=1024,
|
|
)
|
|
balanced_same_area = get_dype_config_for_area(
|
|
width=2048,
|
|
height=1080,
|
|
base_resolution=1024,
|
|
)
|
|
|
|
assert balanced_extreme is not None
|
|
assert extreme is not None
|
|
assert balanced_same_area is not None
|
|
assert extreme.dype_scale < balanced_extreme.dype_scale
|
|
assert extreme.dype_scale < balanced_same_area.dype_scale
|
|
|
|
def test_get_dype_config_for_area_is_closer_to_auto_strength(self):
|
|
area = get_dype_config_for_area(
|
|
width=1728,
|
|
height=1152,
|
|
base_resolution=1024,
|
|
)
|
|
auto = get_dype_config_for_resolution(
|
|
width=1728,
|
|
height=1152,
|
|
base_resolution=1024,
|
|
activation_threshold=1536,
|
|
)
|
|
|
|
assert area is not None
|
|
assert auto is not None
|
|
assert area.dype_scale > auto.dype_scale * 0.9
|
|
assert area.dype_scale < auto.dype_scale * 1.1
|
|
|
|
def test_get_dype_config_for_area_uses_higher_exponent_than_old_curve(self):
|
|
config = get_dype_config_for_area(
|
|
width=1536,
|
|
height=1024,
|
|
base_resolution=1024,
|
|
)
|
|
|
|
assert config is not None
|
|
assert 1.25 <= config.dype_exponent <= 2.0
|
|
|
|
def test_get_dype_config_from_preset_area(self):
|
|
"""Preset AREA should use area-based config."""
|
|
config = get_dype_config_from_preset(
|
|
preset=DYPE_PRESET_AREA,
|
|
width=2048,
|
|
height=1536,
|
|
)
|
|
assert config is not None
|
|
assert config.enable_dype is True
|
|
|
|
def test_get_dype_config_from_preset_off(self):
|
|
"""Preset OFF should return None."""
|
|
config = get_dype_config_from_preset(
|
|
preset=DYPE_PRESET_OFF,
|
|
width=2048,
|
|
height=2048,
|
|
)
|
|
assert config is None
|
|
|
|
def test_get_dype_config_from_preset_auto(self):
|
|
"""Preset AUTO should use resolution-based config."""
|
|
config = get_dype_config_from_preset(
|
|
preset=DYPE_PRESET_AUTO,
|
|
width=2048,
|
|
height=2048,
|
|
)
|
|
assert config is not None
|
|
assert config.enable_dype is True
|
|
|
|
def test_get_dype_config_from_preset_4k(self):
|
|
"""Preset 4K should use 4K settings."""
|
|
config = get_dype_config_from_preset(
|
|
preset=DYPE_PRESET_4K,
|
|
width=3840,
|
|
height=2160,
|
|
)
|
|
assert config is not None
|
|
assert config.enable_dype is True
|
|
|
|
def test_get_dype_config_from_preset_manual_custom_overrides(self):
|
|
"""Custom scale/exponent should override defaults only with 'manual' preset."""
|
|
config = get_dype_config_from_preset(
|
|
preset=DYPE_PRESET_MANUAL,
|
|
width=2048,
|
|
height=2048,
|
|
custom_scale=5.0,
|
|
custom_exponent=10.0,
|
|
)
|
|
assert config is not None
|
|
assert config.dype_scale == 5.0
|
|
assert config.dype_exponent == 10.0
|
|
|
|
def test_get_dype_config_from_preset_4k_ignores_custom(self):
|
|
"""4K preset should ignore custom scale/exponent values."""
|
|
config = get_dype_config_from_preset(
|
|
preset=DYPE_PRESET_4K,
|
|
width=3840,
|
|
height=2160,
|
|
custom_scale=5.0,
|
|
custom_exponent=10.0,
|
|
)
|
|
assert config is not None
|
|
# Custom values should be ignored - preset values used instead
|
|
assert config.dype_scale == 2.0 # 4K preset default
|
|
assert config.dype_exponent == 2.0 # 4K preset default
|
|
|
|
|
|
class TestFrequencyComputation:
|
|
"""Tests for frequency computation functions."""
|
|
|
|
def test_compute_vision_yarn_freqs_shape(self):
|
|
"""Test vision_yarn frequency computation shape."""
|
|
pos = torch.arange(16).unsqueeze(0).float()
|
|
config = DyPEConfig()
|
|
|
|
cos, sin = compute_vision_yarn_freqs(
|
|
pos=pos,
|
|
dim=32,
|
|
theta=10000,
|
|
scale_h=2.0,
|
|
scale_w=2.0,
|
|
current_sigma=0.5,
|
|
dype_config=config,
|
|
)
|
|
|
|
assert cos.shape == sin.shape
|
|
assert cos.shape[0] == 1 # batch
|
|
assert cos.shape[1] == 16 # seq_len
|
|
|
|
def test_compute_vision_yarn_freqs_reverts_to_base_rope_at_zero_sigma(self):
|
|
pos = torch.arange(16).unsqueeze(0).float()
|
|
config = DyPEConfig()
|
|
|
|
dy_cos, dy_sin = compute_vision_yarn_freqs(
|
|
pos=pos,
|
|
dim=32,
|
|
theta=10000,
|
|
scale_h=2.0,
|
|
scale_w=2.0,
|
|
current_sigma=0.0,
|
|
dype_config=config,
|
|
)
|
|
base_cos, base_sin = compute_vision_yarn_freqs(
|
|
pos=pos,
|
|
dim=32,
|
|
theta=10000,
|
|
scale_h=1.0,
|
|
scale_w=1.0,
|
|
current_sigma=0.0,
|
|
dype_config=config,
|
|
)
|
|
|
|
assert torch.allclose(dy_cos, base_cos)
|
|
assert torch.allclose(dy_sin, base_sin)
|