Reverts b015ce3f.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
188 lines
6.2 KiB
Python
188 lines
6.2 KiB
Python
"""H3 tiled decode and the X2 Detail VAE, against a stand-in VAE.
|
|
|
|
No weights: the real decoder is a 32B-era ViT3D. What matters here is what the code
|
|
DOES around it -- which flags it sets and restores, when it refuses, and the pixel layout.
|
|
"""
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _needs_comfy(comfyui):
|
|
"""Imports comfy."""
|
|
|
|
|
|
class _Proj:
|
|
def __init__(self, rows):
|
|
self.weight = torch.zeros(rows, 4)
|
|
|
|
|
|
class _Decoder:
|
|
def __init__(self, rows, patch_t=1, patch=2):
|
|
self.proj_out, self.patch_size_t, self.patch_size, self.out_channels = _Proj(rows), patch_t, patch, 3
|
|
|
|
|
|
class _Inner:
|
|
def __init__(self, rows):
|
|
self.decoder = _Decoder(rows)
|
|
self.tiling, self.tile_size, self.tile_overlap_min = False, 0, 0
|
|
self.pixel_mean, self.pixel_std = torch.arange(3.).view(1, 3, 1, 1, 1), torch.ones(1, 3, 1, 1, 1)
|
|
self.during = None
|
|
self.fail = False
|
|
|
|
def decode_output_shape(self, shape):
|
|
b, _, t, h, w = shape
|
|
return (b, self.decoder.out_channels, t, h * 2, w * 2)
|
|
|
|
def decode(self, x, output_buffer):
|
|
self.during = (self.tiling, self.tile_size, self.tile_overlap_min,
|
|
self.decoder.out_channels, self.pixel_mean.shape[1])
|
|
if self.fail:
|
|
raise RuntimeError("out of memory")
|
|
output_buffer.copy_(torch.arange(output_buffer.numel(), dtype=output_buffer.dtype).view_as(output_buffer))
|
|
|
|
|
|
class _Patcher:
|
|
pass
|
|
|
|
|
|
class _Vae:
|
|
"""What the decode touches. rows = proj_out rows: 12 (patch 2, 3 colours) is stock, 48 is X2."""
|
|
device = torch.device("cpu")
|
|
vae_dtype = torch.float32
|
|
disable_offload = False
|
|
|
|
def __init__(self, rows=12):
|
|
self.first_stage_model = _Inner(rows)
|
|
self.patcher = _Patcher()
|
|
self.plain_calls = 0
|
|
|
|
def throw_exception_if_invalid(self):
|
|
pass
|
|
|
|
def memory_used_decode(self, shape, dtype):
|
|
return 0
|
|
|
|
def vae_output_dtype(self):
|
|
return torch.float32
|
|
|
|
def process_output(self, x):
|
|
return x
|
|
|
|
def decode(self, latent):
|
|
self.plain_calls += 1
|
|
return torch.zeros(1, 2, 4, 4, 3)
|
|
|
|
|
|
@pytest.fixture
|
|
def patched(monkeypatch):
|
|
import comfy.model_management as mm
|
|
import contextlib
|
|
monkeypatch.setattr(mm, "cuda_device_context", lambda d: contextlib.nullcontext())
|
|
monkeypatch.setattr(mm, "load_models_gpu", lambda *a, **k: None)
|
|
monkeypatch.setattr(mm, "intermediate_device", lambda: torch.device("cpu"))
|
|
|
|
|
|
def _mod():
|
|
from modules.models.minimax_h3 import vae_decode
|
|
return vae_decode
|
|
|
|
|
|
def test_x2_ratio_reads_the_decoders_output_rows():
|
|
m = _mod()
|
|
assert m.x2_ratio(_Vae(12)) == 1 # 12 // (1*4) // 3 = 1
|
|
assert m.x2_ratio(_Vae(48)) == 2 # 4 -> sqrt 2
|
|
assert m.x2_ratio(_Vae(27)) == 1 # not a square number of phases
|
|
assert m.x2_ratio(object()) == 1 # unreadable: stock
|
|
|
|
|
|
def test_tiles_are_at_least_256_a_multiple_of_16_with_a_quarter_overlap():
|
|
m = _mod()
|
|
assert m.tiles(64) == (256, 64)
|
|
assert m.tiles(500) == (496, 112)
|
|
assert m.tiles(1024) == (1024, 256)
|
|
|
|
|
|
def test_pixel_unshuffle_layout_is_r_then_g_then_b_phases():
|
|
m = _mod()
|
|
packed = torch.zeros(1, 12, 1, 1, 1) # 3 colours x 2x2 phases
|
|
for color in range(3):
|
|
for phase in range(4):
|
|
packed[0, color * 4 + phase, 0, 0, 0] = color * 10 + phase
|
|
out = m.unpack(packed, 2)
|
|
assert out.shape == (1, 3, 1, 2, 2)
|
|
for color in range(3):
|
|
assert out[0, color, 0].flatten().tolist() == [color * 10 + p for p in range(4)]
|
|
|
|
|
|
def test_decode_fast_sets_the_tile_flags_for_the_call_and_restores_them(patched):
|
|
m = _mod()
|
|
vae = _Vae(12)
|
|
video = torch.zeros(1, 4, 2, 8, 8)
|
|
out = m.decode_fast(vae, video, 300)
|
|
inner = vae.first_stage_model
|
|
assert inner.during == (True, 288, 64, 3, 3) # tile 288 = 300 floored to 16, overlap a quarter
|
|
assert (inner.tiling, inner.tile_size, inner.tile_overlap_min, inner.decoder.out_channels) == (False, 0, 0, 3)
|
|
assert out.shape == (1, 2, 16, 16, 3) # B, T, H, W, C
|
|
|
|
|
|
def test_flags_are_restored_even_when_the_decode_fails(patched):
|
|
m = _mod()
|
|
vae = _Vae(48)
|
|
vae.first_stage_model.fail = True
|
|
with pytest.raises(RuntimeError):
|
|
m.decode_fast(vae, torch.zeros(1, 4, 2, 8, 8), 256)
|
|
inner = vae.first_stage_model
|
|
assert (inner.tiling, inner.decoder.out_channels, inner.pixel_mean.shape[1]) == (False, 3, 3)
|
|
|
|
|
|
def test_an_x2_vae_decodes_packed_then_shuffles_to_double_resolution(patched):
|
|
m = _mod()
|
|
vae = _Vae(48)
|
|
out = m.decode_fast(vae, torch.zeros(1, 4, 2, 8, 8), 256)
|
|
inner = vae.first_stage_model
|
|
assert inner.during == (True, 256, 64, 12, 12) # 12 packed channels, stats repeated per phase
|
|
assert out.shape == (1, 2, 32, 32, 3) # decode_output_shape doubled, then x2 again by the shuffle
|
|
|
|
|
|
def _provider_decode(vae, tile):
|
|
from modules.models import minimax_h3 as h3
|
|
return h3._video_images(vae, torch.zeros(1, 4, 2, 8, 8), tile)
|
|
|
|
|
|
def test_tile_size_zero_on_a_stock_vae_is_the_plain_decode(patched):
|
|
vae = _Vae(12)
|
|
out = _provider_decode(vae, 0)
|
|
assert vae.plain_calls == 1 and vae.first_stage_model.during is None and out.shape == (1, 2, 4, 4, 3)
|
|
|
|
|
|
def test_a_stock_vae_with_tiles_goes_through_the_tiled_decoder(patched):
|
|
vae = _Vae(12)
|
|
_provider_decode(vae, 512)
|
|
assert vae.plain_calls == 0 and vae.first_stage_model.during[:3] == (True, 512, 128)
|
|
|
|
|
|
def test_a_failing_tiled_decode_falls_back_to_plain_and_says_so_once(patched):
|
|
from core import log
|
|
log._reset()
|
|
vae = _Vae(12)
|
|
vae.first_stage_model.fail = True
|
|
out = _provider_decode(vae, 512)
|
|
assert vae.plain_calls == 1 and out.shape == (1, 2, 4, 4, 3)
|
|
assert any("decoded in one piece" in r["message"] for r in log.history())
|
|
|
|
|
|
def test_an_x2_vae_never_falls_back_it_names_itself(patched):
|
|
vae = _Vae(48)
|
|
vae.first_stage_model.fail = True
|
|
with pytest.raises(RuntimeError, match="X2 Detail VAE"):
|
|
_provider_decode(vae, 0)
|
|
assert vae.plain_calls == 0
|
|
|
|
|
|
def test_an_x2_vae_is_tiled_even_when_tiles_are_off(patched):
|
|
vae = _Vae(48)
|
|
_provider_decode(vae, 0)
|
|
assert vae.first_stage_model.during[:2] == (True, 256) and vae.plain_calls == 0
|