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

215 lines
9.4 KiB
Python

"""Tests for the Anima VAE invocations: working-memory estimation, the tiled-decode
decision, and the tiled retry on out-of-memory."""
import math
from unittest.mock import MagicMock, patch
import pytest
import torch
from diffusers.models.autoencoders import AutoencoderKLWan
from invokeai.app.invocations.anima_image_to_latents import AnimaImageToLatentsInvocation
from invokeai.app.invocations.anima_latents_to_image import (
ANIMA_VAE_TILE_SIZE,
ANIMA_VAE_TILE_STRIDE,
AnimaLatentsToImageInvocation,
)
from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_anima
def _mock_wan_vae(dtype: torch.dtype = torch.float16) -> MagicMock:
vae = MagicMock(spec=AutoencoderKLWan)
param = torch.zeros(1, dtype=dtype)
# Return a fresh iterator on every call so the estimator can be called repeatedly.
vae.parameters.side_effect = lambda: iter([param])
return vae
class TestEstimateVaeWorkingMemoryAnima:
def test_untiled_decode_uses_decode_constant_and_scales_latent_dims(self):
latents = torch.zeros(1, 16, 1, 128, 128)
result = estimate_vae_working_memory_anima(
operation="decode", image_tensor=latents, vae=_mock_wan_vae(torch.float16), tile_size=None
)
out_h = out_w = 128 * LATENT_SCALE_FACTOR
assert result == int(out_h * out_w * 2 * 2900)
def test_untiled_encode_uses_encode_constant_and_pixel_dims(self):
image = torch.zeros(1, 3, 1, 1024, 1024)
result = estimate_vae_working_memory_anima(
operation="encode", image_tensor=image, vae=_mock_wan_vae(torch.float16), tile_size=None
)
assert result == int(1024 * 1024 * 2 * 1450)
@pytest.mark.parametrize("latent_hw", [(64, 64), (160, 160)])
def test_tiled_decode_estimate_is_independent_of_image_size(self, latent_hw):
latents = torch.zeros(1, 16, 1, *latent_hw)
result = estimate_vae_working_memory_anima(
operation="decode", image_tensor=latents, vae=_mock_wan_vae(torch.float16), tile_size=512
)
assert result == int(512 * 512 * 2 * 2900 * 1.25)
def test_estimate_scales_with_element_size(self):
latents = torch.zeros(1, 16, 1, 128, 128)
fp16 = estimate_vae_working_memory_anima(
operation="decode", image_tensor=latents, vae=_mock_wan_vae(torch.float16), tile_size=None
)
fp32 = estimate_vae_working_memory_anima(
operation="decode", image_tensor=latents, vae=_mock_wan_vae(torch.float32), tile_size=None
)
assert fp32 == 2 * fp16
class TestUseTiledDecode:
@pytest.mark.parametrize("device_type", ["cpu", "mps"])
def test_non_cuda_never_tiles(self, device_type):
assert AnimaLatentsToImageInvocation._use_tiled_decode(torch.device(device_type), 10**12) is False
def test_cuda_flips_at_70_percent_of_total_vram(self):
total_vram = 8 * 2**30
boundary = 0.7 * total_vram
device = torch.device("cuda")
with patch("torch.cuda.get_device_properties", return_value=MagicMock(total_memory=total_vram)) as mock_props:
assert AnimaLatentsToImageInvocation._use_tiled_decode(device, math.floor(boundary)) is False
assert AnimaLatentsToImageInvocation._use_tiled_decode(device, math.ceil(boundary) + 1) is True
mock_props.assert_called_with(device)
def _build_decode_mocks(latents: torch.Tensor, decoded: torch.Tensor):
"""Mock the Wan VAE decode path: a spec'd AutoencoderKLWan, its LoadedModel wrapper, and the
invocation context, wired so `AnimaLatentsToImageInvocation.invoke` runs end-to-end on CPU."""
vae = _mock_wan_vae(torch.float32)
vae.config.latents_mean = [0.0] * 16
vae.config.latents_std = [1.0] * 16
vae.decode.return_value = (decoded,)
vae_info = MagicMock()
vae_info.model = vae
cm = MagicMock()
cm.__enter__ = MagicMock(return_value=(None, vae))
cm.__exit__ = MagicMock(return_value=None)
vae_info.model_on_device.return_value = cm
context = MagicMock()
context.models.load.return_value = vae_info
context.tensors.load.return_value = latents
image_dto = MagicMock()
image_dto.image_name = "test.png"
image_dto.width = decoded.shape[-1]
image_dto.height = decoded.shape[-2]
context.images.save.return_value = image_dto
return vae, vae_info, context
def _build_l2i_invocation() -> AnimaLatentsToImageInvocation:
return AnimaLatentsToImageInvocation.model_construct(
latents=MagicMock(latents_name="test_latents"),
vae=MagicMock(vae=MagicMock()),
)
class TestAnimaLatentsToImageOomFallback:
@pytest.mark.parametrize(
"oom_error",
[
torch.cuda.OutOfMemoryError("CUDA out of memory. Tried to allocate 5.9 GiB"),
RuntimeError("CUDA error: out of memory"),
RuntimeError("cuDNN error: CUDNN_STATUS_ALLOC_FAILED"),
],
)
def test_untiled_decode_oom_retries_with_tiling(self, oom_error):
decoded = torch.zeros(1, 3, 1, 64, 64)
vae, _, context = _build_decode_mocks(latents=torch.zeros(1, 16, 32, 32), decoded=decoded)
vae.decode.side_effect = [oom_error, (decoded,)]
with patch.object(TorchDevice, "choose_torch_device", return_value=torch.device("cpu")):
result = _build_l2i_invocation().invoke(context)
assert vae.decode.call_count == 2
vae.enable_tiling.assert_called_once_with(
tile_sample_min_height=ANIMA_VAE_TILE_SIZE,
tile_sample_min_width=ANIMA_VAE_TILE_SIZE,
tile_sample_stride_height=ANIMA_VAE_TILE_STRIDE,
tile_sample_stride_width=ANIMA_VAE_TILE_STRIDE,
)
assert result.width == 64
def test_non_oom_runtime_error_propagates_without_retry(self):
vae, _, context = _build_decode_mocks(latents=torch.zeros(1, 16, 32, 32), decoded=torch.zeros(1, 3, 1, 64, 64))
vae.decode.side_effect = RuntimeError("Input type (float) and weight type (half) should be the same")
with patch.object(TorchDevice, "choose_torch_device", return_value=torch.device("cpu")):
with pytest.raises(RuntimeError, match="weight type"):
_build_l2i_invocation().invoke(context)
assert vae.decode.call_count == 1
vae.enable_tiling.assert_not_called()
def test_oom_while_already_tiled_reraises(self):
vae, _, context = _build_decode_mocks(latents=torch.zeros(1, 16, 32, 32), decoded=torch.zeros(1, 3, 1, 64, 64))
vae.decode.side_effect = torch.cuda.OutOfMemoryError("CUDA out of memory")
with (
patch.object(TorchDevice, "choose_torch_device", return_value=torch.device("cpu")),
patch.object(AnimaLatentsToImageInvocation, "_use_tiled_decode", return_value=True),
):
with pytest.raises(torch.cuda.OutOfMemoryError):
_build_l2i_invocation().invoke(context)
# No second attempt: the initial enable_tiling is the only one, and decode is not retried.
assert vae.decode.call_count == 1
vae.enable_tiling.assert_called_once()
def test_decode_requests_estimated_working_memory(self):
decoded = torch.zeros(1, 3, 1, 64, 64)
vae, vae_info, context = _build_decode_mocks(latents=torch.zeros(1, 16, 32, 32), decoded=decoded)
estimation_path = "invokeai.app.invocations.anima_latents_to_image.estimate_vae_working_memory_anima"
expected_memory = 1024 * 1024 * 500
with (
patch.object(TorchDevice, "choose_torch_device", return_value=torch.device("cpu")),
patch(estimation_path, return_value=expected_memory) as mock_estimate,
):
_build_l2i_invocation().invoke(context)
# Called once for the full-decode estimate (tiling decision) and once for the actual request.
assert mock_estimate.call_count == 2
vae_info.model_on_device.assert_called_once_with(working_mem_bytes=expected_memory)
class TestAnimaImageToLatentsEncode:
def test_encode_disables_tiling_and_requests_working_memory(self):
vae = _mock_wan_vae(torch.float32)
vae.config.latents_mean = [0.0] * 16
vae.config.latents_std = [1.0] * 16
mock_dist = MagicMock()
mock_dist.sample.return_value = torch.zeros(1, 16, 1, 4, 4)
vae.encode.return_value = (mock_dist,)
vae_info = MagicMock()
vae_info.model = vae
cm = MagicMock()
cm.__enter__ = MagicMock(return_value=(None, vae))
cm.__exit__ = MagicMock(return_value=None)
vae_info.model_on_device.return_value = cm
estimation_path = "invokeai.app.invocations.anima_image_to_latents.estimate_vae_working_memory_anima"
expected_memory = 1024 * 1024 * 250
with (
patch.object(TorchDevice, "choose_torch_device", return_value=torch.device("cpu")),
patch(estimation_path, return_value=expected_memory) as mock_estimate,
):
latents = AnimaImageToLatentsInvocation.vae_encode(
vae_info=vae_info, image_tensor=torch.zeros(1, 3, 32, 32)
)
# The shared cached VAE may have tiling enabled from a previous decode; encode must reset it.
vae.disable_tiling.assert_called_once()
vae.enable_tiling.assert_not_called()
mock_estimate.assert_called_once()
vae_info.model_on_device.assert_called_once_with(working_mem_bytes=expected_memory)
assert latents.shape == (1, 16, 4, 4)