Kohya_ss_2 / sd-scripts /tests /library /test_flux_train_utils.py
ChBysk's picture
Upload folder using huggingface_hub (part 2)
622d48e verified
Raw
History Blame Contribute Delete
13.4 kB
import pytest
import torch
from unittest.mock import MagicMock, patch
from library.flux_train_utils import (
get_noisy_model_input_and_timesteps,
get_show_timesteps_offset,
)
# Mock classes and functions
class MockNoiseScheduler:
def __init__(self, num_train_timesteps=1000):
self.config = MagicMock()
self.config.num_train_timesteps = num_train_timesteps
self.timesteps = torch.arange(num_train_timesteps, dtype=torch.long)
# Create fixtures for commonly used objects
@pytest.fixture
def args():
args = MagicMock()
args.timestep_sampling = "uniform"
args.weighting_scheme = "uniform"
args.logit_mean = 0.0
args.logit_std = 1.0
args.mode_scale = 1.0
args.sigmoid_scale = 1.0
args.discrete_flow_shift = 3.1582
args.ip_noise_gamma = None
args.ip_noise_gamma_random_strength = False
return args
@pytest.fixture
def noise_scheduler():
return MockNoiseScheduler(num_train_timesteps=1000)
@pytest.fixture
def latents():
return torch.randn(2, 4, 8, 8)
@pytest.fixture
def noise():
return torch.randn(2, 4, 8, 8)
@pytest.fixture
def device():
# return "cuda" if torch.cuda.is_available() else "cpu"
return "cpu"
# Mock the required functions
@pytest.fixture(autouse=True)
def mock_functions():
with (
patch("torch.sigmoid", side_effect=torch.sigmoid),
patch("torch.rand", side_effect=torch.rand),
patch("torch.randn", side_effect=torch.randn),
):
yield
# Test different timestep sampling methods
def test_uniform_sampling(args, noise_scheduler, latents, noise, device):
args.timestep_sampling = "uniform"
dtype = torch.float32
noisy_input, timesteps, sigmas = get_noisy_model_input_and_timesteps(args, noise_scheduler, latents, noise, device, dtype)
assert noisy_input.shape == latents.shape
assert timesteps.shape == (latents.shape[0],)
assert sigmas.shape == (latents.shape[0], 1, 1, 1)
assert noisy_input.dtype == dtype
assert timesteps.dtype == dtype
def test_sigmoid_sampling(args, noise_scheduler, latents, noise, device):
args.timestep_sampling = "sigmoid"
args.sigmoid_scale = 1.0
dtype = torch.float32
noisy_input, timesteps, sigmas = get_noisy_model_input_and_timesteps(args, noise_scheduler, latents, noise, device, dtype)
assert noisy_input.shape == latents.shape
assert timesteps.shape == (latents.shape[0],)
assert sigmas.shape == (latents.shape[0], 1, 1, 1)
def test_shift_sampling(args, noise_scheduler, latents, noise, device):
args.timestep_sampling = "shift"
args.sigmoid_scale = 1.0
args.discrete_flow_shift = 3.1582
dtype = torch.float32
noisy_input, timesteps, sigmas = get_noisy_model_input_and_timesteps(args, noise_scheduler, latents, noise, device, dtype)
assert noisy_input.shape == latents.shape
assert timesteps.shape == (latents.shape[0],)
assert sigmas.shape == (latents.shape[0], 1, 1, 1)
def test_flux_shift_sampling(args, noise_scheduler, latents, noise, device):
args.timestep_sampling = "flux_shift"
args.sigmoid_scale = 1.0
dtype = torch.float32
noisy_input, timesteps, sigmas = get_noisy_model_input_and_timesteps(args, noise_scheduler, latents, noise, device, dtype)
assert noisy_input.shape == latents.shape
assert timesteps.shape == (latents.shape[0],)
assert sigmas.shape == (latents.shape[0], 1, 1, 1)
def test_weighting_scheme(args, noise_scheduler, latents, noise, device):
# Mock the necessary functions for this specific test
with patch("library.flux_train_utils.compute_density_for_timestep_sampling",
return_value=torch.tensor([0.3, 0.7], device=device)), \
patch("library.flux_train_utils.get_sigmas",
return_value=torch.tensor([[0.3], [0.7]], device=device).view(-1, 1, 1, 1)):
args.timestep_sampling = "other" # Will trigger the weighting scheme path
args.weighting_scheme = "uniform"
args.logit_mean = 0.0
args.logit_std = 1.0
args.mode_scale = 1.0
dtype = torch.float32
noisy_input, timesteps, sigmas = get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, device, dtype
)
assert noisy_input.shape == latents.shape
assert timesteps.shape == (latents.shape[0],)
assert sigmas.shape == (latents.shape[0], 1, 1, 1)
# Test IP noise options
def test_with_ip_noise(args, noise_scheduler, latents, noise, device):
args.ip_noise_gamma = 0.5
args.ip_noise_gamma_random_strength = False
dtype = torch.float32
noisy_input, timesteps, sigmas = get_noisy_model_input_and_timesteps(args, noise_scheduler, latents, noise, device, dtype)
assert noisy_input.shape == latents.shape
assert timesteps.shape == (latents.shape[0],)
assert sigmas.shape == (latents.shape[0], 1, 1, 1)
def test_with_random_ip_noise(args, noise_scheduler, latents, noise, device):
args.ip_noise_gamma = 0.1
args.ip_noise_gamma_random_strength = True
dtype = torch.float32
noisy_input, timesteps, sigmas = get_noisy_model_input_and_timesteps(args, noise_scheduler, latents, noise, device, dtype)
assert noisy_input.shape == latents.shape
assert timesteps.shape == (latents.shape[0],)
assert sigmas.shape == (latents.shape[0], 1, 1, 1)
# Test different data types
def test_float16_dtype(args, noise_scheduler, latents, noise, device):
dtype = torch.float16
noisy_input, timesteps, sigmas = get_noisy_model_input_and_timesteps(args, noise_scheduler, latents, noise, device, dtype)
assert noisy_input.dtype == dtype
assert timesteps.dtype == dtype
# Test different batch sizes
def test_different_batch_size(args, noise_scheduler, device):
latents = torch.randn(5, 4, 8, 8) # batch size of 5
noise = torch.randn(5, 4, 8, 8)
dtype = torch.float32
noisy_input, timesteps, sigmas = get_noisy_model_input_and_timesteps(args, noise_scheduler, latents, noise, device, dtype)
assert noisy_input.shape == latents.shape
assert timesteps.shape == (5,)
assert sigmas.shape == (5, 1, 1, 1)
# Test different image sizes
def test_different_image_size(args, noise_scheduler, device):
latents = torch.randn(2, 4, 16, 16) # larger image size
noise = torch.randn(2, 4, 16, 16)
dtype = torch.float32
noisy_input, timesteps, sigmas = get_noisy_model_input_and_timesteps(args, noise_scheduler, latents, noise, device, dtype)
assert noisy_input.shape == latents.shape
assert timesteps.shape == (2,)
assert sigmas.shape == (2, 1, 1, 1)
# Test edge cases
def test_zero_batch_size(args, noise_scheduler, device):
with pytest.raises(AssertionError): # expecting an error with zero batch size
latents = torch.randn(0, 4, 8, 8)
noise = torch.randn(0, 4, 8, 8)
dtype = torch.float32
get_noisy_model_input_and_timesteps(args, noise_scheduler, latents, noise, device, dtype)
def test_different_timestep_count(args, device):
noise_scheduler = MockNoiseScheduler(num_train_timesteps=500) # different timestep count
latents = torch.randn(2, 4, 8, 8)
noise = torch.randn(2, 4, 8, 8)
dtype = torch.float32
noisy_input, timesteps, sigmas = get_noisy_model_input_and_timesteps(args, noise_scheduler, latents, noise, device, dtype)
assert noisy_input.shape == latents.shape
assert timesteps.shape == (2,)
# Check that timesteps are within the proper range
assert torch.all(timesteps < 500)
# Tests for timestep_sampling_offset
class TestTimestepSamplingOffset:
"""Regression tests for per-subset timestep sampling offset."""
@pytest.mark.parametrize("mode", ["sigmoid", "shift", "flux_shift"])
def test_none_offset_matches_baseline(self, args, noise_scheduler, device, mode):
"""offset=None should produce identical results to no offset."""
args.timestep_sampling = mode
args.sigmoid_scale = 1.0
bsz = 64
latents = torch.randn(bsz, 4, 8, 8)
noise = torch.randn(bsz, 4, 8, 8)
dtype = torch.float32
torch.manual_seed(42)
_, ts_baseline, _ = get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, device, dtype
)
torch.manual_seed(42)
_, ts_none, _ = get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, device, dtype, timestep_sampling_offset=None
)
assert torch.allclose(ts_baseline, ts_none)
@pytest.mark.parametrize("mode", ["sigmoid", "shift", "flux_shift"])
def test_zero_offset_matches_baseline(self, args, noise_scheduler, device, mode):
"""offset=0.0 should produce identical results to no offset."""
args.timestep_sampling = mode
args.sigmoid_scale = 1.0
bsz = 64
latents = torch.randn(bsz, 4, 8, 8)
noise = torch.randn(bsz, 4, 8, 8)
dtype = torch.float32
torch.manual_seed(42)
_, ts_baseline, _ = get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, device, dtype
)
torch.manual_seed(42)
_, ts_zero, _ = get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, device, dtype,
timestep_sampling_offset=torch.zeros(bsz)
)
assert torch.allclose(ts_baseline, ts_zero)
@pytest.mark.parametrize("mode", ["sigmoid", "shift", "flux_shift"])
def test_positive_offset_increases_mean_timestep(self, args, noise_scheduler, device, mode):
"""Positive offset should shift timestep distribution upward (higher noise)."""
args.timestep_sampling = mode
args.sigmoid_scale = 1.0
bsz = 256
latents = torch.randn(bsz, 4, 8, 8)
noise = torch.randn(bsz, 4, 8, 8)
dtype = torch.float32
torch.manual_seed(42)
_, ts_baseline, _ = get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, device, dtype
)
torch.manual_seed(42)
_, ts_offset, _ = get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, device, dtype,
timestep_sampling_offset=torch.full((bsz,), 1.0)
)
assert ts_offset.mean() > ts_baseline.mean()
def test_offset_does_not_affect_uniform(self, args, noise_scheduler, device):
"""Uniform sampling ignores offset (offset code path is not reached)."""
args.timestep_sampling = "uniform"
bsz = 64
latents = torch.randn(bsz, 4, 8, 8)
noise = torch.randn(bsz, 4, 8, 8)
dtype = torch.float32
torch.manual_seed(42)
_, ts_baseline, _ = get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, device, dtype
)
torch.manual_seed(42)
_, ts_offset, _ = get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, device, dtype,
timestep_sampling_offset=torch.full((bsz,), 1.0)
)
assert torch.allclose(ts_baseline, ts_offset)
def test_per_sample_offset_broadcasting(self, args, noise_scheduler, device):
"""Different offsets per sample should produce different shifts."""
args.timestep_sampling = "sigmoid"
args.sigmoid_scale = 1.0
bsz = 4
latents = torch.randn(bsz, 4, 8, 8)
noise = torch.randn(bsz, 4, 8, 8)
dtype = torch.float32
offset = torch.tensor([-1.0, 0.0, 0.0, 1.0])
torch.manual_seed(42)
_, ts_baseline, _ = get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, device, dtype
)
torch.manual_seed(42)
_, ts_offset, _ = get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, device, dtype,
timestep_sampling_offset=offset
)
# Samples with offset=0 should match baseline
assert torch.allclose(ts_offset[1], ts_baseline[1])
assert torch.allclose(ts_offset[2], ts_baseline[2])
# Negative offset → lower timestep, positive → higher
assert ts_offset[0] < ts_baseline[0]
assert ts_offset[3] > ts_baseline[3]
class TestGetShowTimestepsOffset:
"""Tests for the --show_timesteps_offset resolution helper."""
@pytest.mark.parametrize("mode", ["sigmoid", "shift", "flux_shift"])
def test_offset_applied_for_supported_modes(self, args, mode):
args.timestep_sampling = mode
args.show_timesteps_offset = -0.5
offset, note = get_show_timesteps_offset(args)
assert offset == -0.5
assert "IGNORED" not in note
assert "-0.5" in note
@pytest.mark.parametrize("mode", ["uniform", "sigma"])
def test_offset_ignored_for_unsupported_modes(self, args, mode):
args.timestep_sampling = mode
args.show_timesteps_offset = -0.5
offset, note = get_show_timesteps_offset(args)
assert offset is None
assert "IGNORED" in note
def test_zero_offset_returns_none(self, args):
args.timestep_sampling = "shift"
args.show_timesteps_offset = 0.0
offset, note = get_show_timesteps_offset(args)
assert offset is None
assert note == ""