- Extract detect_architecture() to shared rocm_nodes/utils/architecture.py - ROCm KSampler now auto-detects GPU architecture + model type at runtime: - Flow-matching models (z-image, ernie, ideogram4) logged and optimized - Pixel-space models (z-image-turbo) recognized — no VAE decode needed - High-memory models (ideogram4, factor 11.6x) get automatic cleanup - Architecture-aware backend settings (TF32, fp16 accumulation) - ROCmSamplerPerformanceMonitor now model-type + architecture aware - ROCmVAEPerformanceMonitor uses shared architecture detection - Fix ProgressBar mock in test conftest - Add flow-matching and pixel-space model detection tests - Update KSampler/PerfMonitor README docs
376 lines
11 KiB
Python
376 lines
11 KiB
Python
"""
|
|
Test suite for ROCM Ninodes
|
|
Comprehensive testing infrastructure for all ROCM-optimized nodes
|
|
"""
|
|
import sys
|
|
import os
|
|
import pytest
|
|
import torch
|
|
import numpy as np
|
|
from pathlib import Path
|
|
from unittest.mock import Mock
|
|
|
|
# Add ComfyUI to path
|
|
sys.path.insert(0, '/home/nino/ComfyUI')
|
|
|
|
# Mock ComfyUI modules BEFORE importing nodes
|
|
import types
|
|
comfy = types.ModuleType('comfy')
|
|
comfy.model_management = types.ModuleType('comfy.model_management')
|
|
comfy.utils = types.ModuleType('comfy.utils')
|
|
comfy.sd = types.ModuleType('comfy.sd')
|
|
comfy.samplers = types.ModuleType('comfy.samplers')
|
|
comfy.sample = types.ModuleType('comfy.sample')
|
|
|
|
# Mock comfy.cli_args and latent_preview so rocm_nodes.core.vae can be imported
|
|
comfy.cli_args = types.ModuleType('comfy.cli_args')
|
|
comfy.cli_args.args = type('Args', (), {'preview_method': None, 'base_directory': None})()
|
|
comfy.cli_args.LatentPreviewMethod = type('LatentPreviewMethod', (), {})
|
|
latent_preview = types.ModuleType('latent_preview')
|
|
latent_preview.get_previewer = lambda *args, **kwargs: None
|
|
|
|
# Mock WAN and LTX packages
|
|
comfy.ldm = types.ModuleType('comfy.ldm')
|
|
comfy.ldm.wan = types.ModuleType('comfy.ldm.wan')
|
|
comfy.ldm.wan.vae = types.ModuleType('comfy.ldm.wan.vae')
|
|
comfy.ldm.wan.vae2_2 = types.ModuleType('comfy.ldm.wan.vae2_2')
|
|
|
|
class MockWanVAE:
|
|
pass
|
|
|
|
class MockWanVAE2_2:
|
|
pass
|
|
|
|
comfy.ldm.wan.vae.WanVAE = MockWanVAE
|
|
comfy.ldm.wan.vae2_2.WanVAE = MockWanVAE2_2
|
|
|
|
comfy.ldm.lightricks = types.ModuleType('comfy.ldm.lightricks')
|
|
comfy.ldm.lightricks.vae = types.ModuleType('comfy.ldm.lightricks.vae')
|
|
comfy.ldm.lightricks.vae.causal_video_autoencoder = types.ModuleType('comfy.ldm.lightricks.vae.causal_video_autoencoder')
|
|
|
|
class MockVideoVAE:
|
|
pass
|
|
|
|
comfy.ldm.lightricks.vae.causal_video_autoencoder.VideoVAE = MockVideoVAE
|
|
|
|
# Add modules to sys.modules BEFORE any imports
|
|
sys.modules['comfy'] = comfy
|
|
sys.modules['comfy.model_management'] = comfy.model_management
|
|
sys.modules['comfy.utils'] = comfy.utils
|
|
sys.modules['comfy.sd'] = comfy.sd
|
|
sys.modules['comfy.samplers'] = comfy.samplers
|
|
sys.modules['comfy.sample'] = comfy.sample
|
|
sys.modules['comfy.cli_args'] = comfy.cli_args
|
|
sys.modules['latent_preview'] = latent_preview
|
|
sys.modules['comfy.ldm'] = comfy.ldm
|
|
sys.modules['comfy.ldm.wan'] = comfy.ldm.wan
|
|
sys.modules['comfy.ldm.wan.vae'] = comfy.ldm.wan.vae
|
|
sys.modules['comfy.ldm.wan.vae2_2'] = comfy.ldm.wan.vae2_2
|
|
sys.modules['comfy.ldm.lightricks'] = comfy.ldm.lightricks
|
|
sys.modules['comfy.ldm.lightricks.vae'] = comfy.ldm.lightricks.vae
|
|
sys.modules['comfy.ldm.lightricks.vae.causal_video_autoencoder'] = comfy.ldm.lightricks.vae.causal_video_autoencoder
|
|
|
|
# Mock functions
|
|
comfy.model_management.load_models_gpu = lambda *args, **kwargs: None
|
|
comfy.sd.load_checkpoint_guess_config = lambda *args, **kwargs: (Mock(), Mock(), Mock())
|
|
comfy.model_management.get_free_memory = lambda *args, **kwargs: 16 * 1024**3
|
|
comfy.model_management.minimum_inference_memory = lambda *args, **kwargs: 8 * 1024**3
|
|
comfy.model_management.extra_reserved_memory = lambda *args, **kwargs: 0
|
|
comfy.utils.tiled_scale = lambda *args, **kwargs: (torch.randn(1, 3, 512, 512), 512, 512)
|
|
|
|
class MockProgressBar:
|
|
def __init__(self, total, node_id=None):
|
|
self.total = total
|
|
self.current = 0
|
|
def update_absolute(self, value, total=None, preview=None):
|
|
self.current = value
|
|
def update(self, value):
|
|
pass
|
|
|
|
comfy.utils.ProgressBar = MockProgressBar
|
|
|
|
# Need to mock folder_paths for constants import
|
|
folder_paths = types.ModuleType('folder_paths')
|
|
sys.modules['folder_paths'] = folder_paths
|
|
|
|
|
|
class MockModel:
|
|
def __init__(self):
|
|
self.model = None
|
|
|
|
|
|
class MockPatcher:
|
|
def __init__(self):
|
|
self.model_patches_models = lambda: []
|
|
self.current_loaded_device = lambda: torch.device('cpu')
|
|
self.model_size = lambda: 1024
|
|
self.loaded_size = lambda: 1024
|
|
self.model_patches_to = lambda x: x
|
|
self.model_dtype = lambda: torch.float32
|
|
self.partially_load = lambda device, extra_memory, force_patch_weights=False: True
|
|
self.is_clone = lambda: False
|
|
self.parent = None
|
|
self.load_device = torch.device('cpu')
|
|
self.model = MockModel()
|
|
self.device = torch.device('cpu')
|
|
|
|
def model_memory_required(self, device):
|
|
return 1024
|
|
|
|
def model_load(self, lowvram_model_memory, force_patch_weights=False):
|
|
pass
|
|
|
|
def model_use_more_vram(self, use_more_vram, force_patch_weights=False):
|
|
pass
|
|
|
|
|
|
class MockFirstStageModel:
|
|
def __init__(self):
|
|
self.dtype = torch.float32
|
|
|
|
def to(self, dtype):
|
|
self.dtype = dtype
|
|
return self
|
|
|
|
def decode(self, samples):
|
|
if len(samples.shape) == 5:
|
|
B, C, T, H, W = samples.shape
|
|
return torch.randn(B, T, H*8, W*8, 3)
|
|
else:
|
|
B, C, H, W = samples.shape
|
|
return torch.randn(B, 3, H*8, W*8)
|
|
|
|
|
|
class MockVAE:
|
|
def __init__(self):
|
|
self.device = torch.device('cpu')
|
|
self.first_stage_model = MockFirstStageModel()
|
|
self.vae_dtype = torch.float32
|
|
self.patcher = MockPatcher()
|
|
self.output_device = torch.device('cpu')
|
|
self.latent_channels = 4
|
|
|
|
def process_output(self, x):
|
|
return x.permute(0, 2, 3, 1)
|
|
|
|
def memory_used_decode(self, shape, dtype):
|
|
return 1024
|
|
|
|
def spacial_compression_decode(self, shape=None):
|
|
return 8
|
|
|
|
def temporal_compression_decode(self, shape=None):
|
|
return 1
|
|
|
|
def decode_tiled(self, samples, tile_size, overlap):
|
|
B, C, H, W = samples.shape
|
|
return torch.randn(B, 3, H*8, W*8)
|
|
|
|
def decode(self, samples):
|
|
if len(samples.shape) == 5:
|
|
B, C, T, H, W = samples.shape
|
|
return torch.randn(B, T, H*8, W*8, 3)
|
|
else:
|
|
B, C, H, W = samples.shape
|
|
return torch.randn(B, H*8, W*8, 3)
|
|
|
|
|
|
class MockLTXFirstStageModel:
|
|
"""Mocks the Lightricks VideoVAE first_stage_model behavior"""
|
|
def __init__(self):
|
|
self.dtype = torch.float16
|
|
|
|
def to(self, dtype):
|
|
self.dtype = dtype
|
|
return self
|
|
|
|
def decode(self, samples):
|
|
B, C, T, H, W = samples.shape
|
|
return torch.randn(B, 3, 1 + (T - 1) * 8, H * 32, W * 32)
|
|
|
|
def state_dict(self):
|
|
return {
|
|
"decoder.up_blocks.0.res_blocks.0.conv1.conv.weight": torch.randn(1024, 128, 3, 3),
|
|
}
|
|
|
|
|
|
class MockLTXVAE:
|
|
"""Mock VAE that mimics Lightricks LTX Video VAE (latent_channels=128)"""
|
|
def __init__(self):
|
|
self.device = torch.device('cpu')
|
|
self.first_stage_model = MockLTXFirstStageModel()
|
|
self.vae_dtype = torch.float16
|
|
self.patcher = MockPatcher()
|
|
self.output_device = torch.device('cpu')
|
|
self.latent_channels = 128
|
|
|
|
def process_output(self, x):
|
|
return x
|
|
|
|
def memory_used_decode(self, shape, dtype):
|
|
return 1024 * 1024
|
|
|
|
def spacial_compression_decode(self, shape=None):
|
|
return 32
|
|
|
|
def temporal_compression_decode(self, shape=None):
|
|
return 8
|
|
|
|
def decode(self, samples):
|
|
B, C, T, H, W = samples.shape
|
|
out_frames = 1 + (T - 1) * 8
|
|
return torch.randn(B, out_frames, H * 32, W * 32, 3)
|
|
|
|
|
|
class MockPixelSpaceFirstStageModel:
|
|
def __init__(self):
|
|
self.dtype = torch.float32
|
|
|
|
def to(self, dtype):
|
|
self.dtype = dtype
|
|
return self
|
|
|
|
def decode(self, samples):
|
|
return samples
|
|
|
|
|
|
class MockPixelSpaceVAE:
|
|
"""Mock VAE that mimics z-image pixel-space passthrough (latent_channels=3)"""
|
|
def __init__(self):
|
|
self.device = torch.device('cpu')
|
|
self.first_stage_model = MockPixelSpaceFirstStageModel()
|
|
self.vae_dtype = torch.float32
|
|
self.patcher = MockPatcher()
|
|
self.output_device = torch.device('cpu')
|
|
self.latent_channels = 3
|
|
|
|
def process_output(self, x):
|
|
return x
|
|
|
|
def memory_used_decode(self, shape, dtype):
|
|
return 1024
|
|
|
|
def spacial_compression_decode(self, shape=None):
|
|
return 1
|
|
|
|
def temporal_compression_decode(self, shape=None):
|
|
return None
|
|
|
|
def decode(self, samples):
|
|
return samples
|
|
|
|
|
|
class MockWanFirstStageModel(MockWanVAE):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.dtype = torch.float32
|
|
|
|
def to(self, dtype):
|
|
self.dtype = dtype
|
|
return self
|
|
|
|
def decode(self, samples):
|
|
B, C, T, H, W = samples.shape
|
|
return torch.randn(B, 3, T, H * 8, W * 8)
|
|
|
|
|
|
class MockWanVAE:
|
|
"""Mock VAE that mimics WAN VAE (latent_channels=16)"""
|
|
def __init__(self):
|
|
self.device = torch.device('cpu')
|
|
self.first_stage_model = MockWanFirstStageModel()
|
|
self.vae_dtype = torch.float32
|
|
self.patcher = MockPatcher()
|
|
self.output_device = torch.device('cpu')
|
|
self.latent_channels = 16
|
|
|
|
def process_output(self, x):
|
|
return x
|
|
|
|
def memory_used_decode(self, shape, dtype):
|
|
return 1024 * 1024
|
|
|
|
def spacial_compression_decode(self, shape=None):
|
|
return 8
|
|
|
|
def temporal_compression_decode(self, shape=None):
|
|
return 4
|
|
|
|
def decode(self, samples):
|
|
B, C, T, H, W = samples.shape
|
|
return torch.randn(B, T, H * 8, W * 8, 3)
|
|
|
|
|
|
# Fixtures
|
|
@pytest.fixture
|
|
def sample_vae():
|
|
"""Provide a mock VAE for testing"""
|
|
return MockVAE()
|
|
|
|
@pytest.fixture
|
|
def sample_latent():
|
|
"""Provide a sample latent tensor"""
|
|
return {
|
|
"samples": torch.randn(1, 4, 32, 32) # 256x256 image
|
|
}
|
|
|
|
@pytest.fixture
|
|
def sample_video_latent():
|
|
"""Provide a sample video latent tensor"""
|
|
return {
|
|
"samples": torch.randn(1, 4, 8, 32, 32) # 8 frames, 256x256
|
|
}
|
|
|
|
@pytest.fixture
|
|
def sample_ltx_vae():
|
|
"""Provide a mock LTX Video VAE"""
|
|
return MockLTXVAE()
|
|
|
|
@pytest.fixture
|
|
def sample_ltx_latent():
|
|
"""Provide a sample LTX video latent (128 channels, 5D)"""
|
|
return {
|
|
"samples": torch.randn(1, 128, 8, 4, 6) # [B, 128, T, H, W]
|
|
}
|
|
|
|
@pytest.fixture
|
|
def sample_wan_vae():
|
|
"""Provide a mock WAN VAE"""
|
|
return MockWanVAE()
|
|
|
|
@pytest.fixture
|
|
def sample_wan_latent():
|
|
"""Provide a sample WAN video latent (16 channels, 5D)"""
|
|
return {
|
|
"samples": torch.randn(1, 16, 8, 32, 32) # [B, 16, T, H, W]
|
|
}
|
|
|
|
@pytest.fixture
|
|
def sample_pixel_vae():
|
|
"""Provide a mock pixel-space VAE (z-image style)"""
|
|
return MockPixelSpaceVAE()
|
|
|
|
@pytest.fixture
|
|
def sample_pixel_latent():
|
|
"""Provide a sample pixel-space latent (3 channels, already pixels)"""
|
|
return {
|
|
"samples": torch.randn(1, 3, 64, 64) # Already RGB pixels
|
|
}
|
|
|
|
@pytest.fixture
|
|
def sample_model():
|
|
"""Provide a mock model for testing"""
|
|
return MockModel()
|
|
|
|
@pytest.fixture
|
|
def sample_conditioning():
|
|
"""Provide sample conditioning for testing"""
|
|
return {
|
|
"conditioning": torch.randn(1, 77, 768)
|
|
}
|
|
|
|
@pytest.fixture
|
|
def is_amd_gpu():
|
|
"""Check if running on AMD GPU"""
|
|
return torch.cuda.is_available()
|