e3c8979e8d
- cmd/fn/doctor.go - cmd/fn/main.go - cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt - cpp/apps/primitives_gallery/playground/tables/data_table.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.h - cpp/apps/primitives_gallery/playground/tables/self_test.cpp - cpp/apps/primitives_gallery/playground/tables/tql.cpp - cpp/apps/primitives_gallery/playground/tables/viz.cpp - cpp/apps/primitives_gallery/playground/tables/viz.h - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
213 lines
8.1 KiB
Python
213 lines
8.1 KiB
Python
"""Tests para el backend diffusers: load_pipeline, set_scheduler, generate, unload."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import os
|
|
import time
|
|
|
|
import pytest
|
|
|
|
# Ajustar path para importar desde python/functions/ml/
|
|
_ML_PATH = os.path.join(
|
|
os.path.dirname(__file__), "..", "..",
|
|
)
|
|
sys.path.insert(0, os.path.abspath(_ML_PATH))
|
|
|
|
# Importaciones lazy de torch y diffusers — las omitimos si no estan disponibles.
|
|
torch = pytest.importorskip("torch", reason="torch no instalado — skip tests diffusers")
|
|
pytest.importorskip("diffusers", reason="diffusers no instalado — skip tests diffusers")
|
|
|
|
from ml.model_ref import ModelRef
|
|
from ml.generation_config import GenerationConfig
|
|
from ml.image_gen_result import ImageGenResult
|
|
from ml.diffusers_load_pipeline import diffusers_load_pipeline, _clear_pipeline_cache
|
|
from ml.diffusers_set_scheduler import diffusers_set_scheduler
|
|
from ml.diffusers_unload import diffusers_unload
|
|
|
|
# diffusers_generate importa image_gen_result sin prefijo de paquete.
|
|
# Para evitar el double-import problem (ml.image_gen_result != image_gen_result),
|
|
# forzamos que sys.modules["image_gen_result"] apunte al modulo ya cargado
|
|
# como ml.image_gen_result antes de importar diffusers_generate.
|
|
import sys as _sys
|
|
import ml.image_gen_result as _igr_module
|
|
import ml.generation_config as _gcfg_module
|
|
import ml.genconfig_to_diffusers_kwargs as _gkwargs_module
|
|
for _alias, _mod in [
|
|
("image_gen_result", _igr_module),
|
|
("generation_config", _gcfg_module),
|
|
("genconfig_to_diffusers_kwargs", _gkwargs_module),
|
|
]:
|
|
if _alias not in _sys.modules:
|
|
_sys.modules[_alias] = _mod
|
|
|
|
from ml.diffusers_generate import diffusers_generate
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constantes
|
|
# ---------------------------------------------------------------------------
|
|
SD_TURBO_PATH = "/home/lucas/vaults/imagegen_models/diffusers/sd-turbo"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.fixture(scope="session")
|
|
def sd_turbo_model() -> ModelRef:
|
|
"""ModelRef apuntando a SD Turbo local."""
|
|
if not os.path.isdir(SD_TURBO_PATH):
|
|
pytest.skip(f"SD Turbo no encontrado en {SD_TURBO_PATH}")
|
|
return ModelRef(
|
|
name="sd-turbo",
|
|
model_type="sd15",
|
|
quantization="fp16",
|
|
path=SD_TURBO_PATH,
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def loaded_pipe(sd_turbo_model: ModelRef):
|
|
"""Pipeline SD Turbo cargado una sola vez para toda la sesion de tests."""
|
|
# Intentar fp16 primero; si falla (no hay variante fp16) usar fp32
|
|
try:
|
|
pipe = diffusers_load_pipeline(sd_turbo_model, device="auto", dtype="fp16")
|
|
except Exception:
|
|
_clear_pipeline_cache()
|
|
pipe = diffusers_load_pipeline(sd_turbo_model, device="auto", dtype="fp32")
|
|
yield pipe
|
|
# Teardown: liberar al final de la sesion
|
|
diffusers_unload(None)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def sd_turbo_cfg(sd_turbo_model: ModelRef) -> GenerationConfig:
|
|
"""GenerationConfig minimo para SD Turbo (1 step, 512x512)."""
|
|
return GenerationConfig(
|
|
prompt="a simple red circle on white background",
|
|
negative_prompt=None,
|
|
seed=42,
|
|
steps=1,
|
|
cfg_scale=0.0,
|
|
sampler="euler",
|
|
width=512,
|
|
height=512,
|
|
model=sd_turbo_model,
|
|
loras=[],
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test: carga pipeline y retorna callable
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_load_pipeline_returns_callable(sd_turbo_model: ModelRef) -> None:
|
|
"""carga pipeline y retorna callable"""
|
|
_clear_pipeline_cache()
|
|
pipe = diffusers_load_pipeline(sd_turbo_model, device="auto", dtype="fp16")
|
|
assert callable(pipe), "El pipeline debe ser callable"
|
|
assert hasattr(pipe, "scheduler"), "El pipeline debe tener atributo scheduler"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test: segunda carga usa cache (< 100ms)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_load_pipeline_caches(sd_turbo_model: ModelRef) -> None:
|
|
"""segunda carga usa cache (< 100ms)"""
|
|
# Primera carga (puede tardar varios segundos)
|
|
_clear_pipeline_cache()
|
|
_ = diffusers_load_pipeline(sd_turbo_model, device="auto", dtype="fp16")
|
|
|
|
# Segunda carga debe ser cache hit
|
|
t0 = time.perf_counter()
|
|
pipe2 = diffusers_load_pipeline(sd_turbo_model, device="auto", dtype="fp16")
|
|
elapsed_ms = (time.perf_counter() - t0) * 1000
|
|
|
|
assert elapsed_ms < 100, (
|
|
f"Segunda carga tardo {elapsed_ms:.1f}ms (esperado < 100ms — debe ser cache hit)"
|
|
)
|
|
assert pipe2 is not None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test: set_scheduler cambia la clase del scheduler
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_set_scheduler_changes_scheduler_class(loaded_pipe) -> None:
|
|
"""euler cambia scheduler a EulerDiscreteScheduler"""
|
|
pipe = diffusers_set_scheduler(loaded_pipe, "euler")
|
|
scheduler_name = type(pipe.scheduler).__name__
|
|
assert scheduler_name == "EulerDiscreteScheduler", (
|
|
f"Esperado EulerDiscreteScheduler, obtenido {scheduler_name}"
|
|
)
|
|
|
|
|
|
def test_set_scheduler_euler_a(loaded_pipe) -> None:
|
|
"""euler_a cambia scheduler a EulerAncestralDiscreteScheduler"""
|
|
pipe = diffusers_set_scheduler(loaded_pipe, "euler_a")
|
|
scheduler_name = type(pipe.scheduler).__name__
|
|
assert scheduler_name == "EulerAncestralDiscreteScheduler", (
|
|
f"Esperado EulerAncestralDiscreteScheduler, obtenido {scheduler_name}"
|
|
)
|
|
# Restaurar euler para no afectar otros tests
|
|
diffusers_set_scheduler(loaded_pipe, "euler")
|
|
|
|
|
|
def test_set_scheduler_invalid_raises_value_error(loaded_pipe) -> None:
|
|
"""sampler invalido lanza ValueError"""
|
|
with pytest.raises(ValueError, match="no soportado"):
|
|
diffusers_set_scheduler(loaded_pipe, "nonexistent_sampler_xyz")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test: genera imagen retorna ImageGenResult
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_generate_returns_image_gen_result(
|
|
loaded_pipe, sd_turbo_cfg: GenerationConfig
|
|
) -> None:
|
|
"""genera imagen retorna ImageGenResult"""
|
|
result = diffusers_generate(loaded_pipe, sd_turbo_cfg)
|
|
|
|
assert isinstance(result, ImageGenResult), (
|
|
f"Esperado ImageGenResult, obtenido {type(result)}"
|
|
)
|
|
assert result.image is not None, "result.image no debe ser None"
|
|
assert result.duration_ms > 0, (
|
|
f"duration_ms debe ser positivo, obtenido {result.duration_ms}"
|
|
)
|
|
assert "backend" in result.meta, "meta debe tener key 'backend'"
|
|
assert result.meta["backend"] == "diffusers", (
|
|
f"meta['backend'] debe ser 'diffusers', obtenido {result.meta['backend']}"
|
|
)
|
|
assert "model" in result.meta, "meta debe tener key 'model'"
|
|
|
|
# Verificar que la imagen tiene las dimensiones correctas
|
|
w, h = result.image.size
|
|
assert w == sd_turbo_cfg.width and h == sd_turbo_cfg.height, (
|
|
f"Imagen esperada {sd_turbo_cfg.width}x{sd_turbo_cfg.height}, "
|
|
f"obtenida {w}x{h}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test: unload limpia cache cuda si disponible
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_unload_clears_cuda() -> None:
|
|
"""unload None limpia cache cuda si disponible"""
|
|
cuda_available = torch.cuda.is_available()
|
|
|
|
# Limpiar cache — no debe lanzar excepcion independientemente de si hay CUDA
|
|
diffusers_unload(None)
|
|
|
|
if cuda_available:
|
|
# Despues de empty_cache, la memoria reservada por el allocator baja
|
|
# No podemos asumir que sea 0 (otros tensores pueden estar vivos),
|
|
# pero la llamada debe completarse sin error.
|
|
reserved = torch.cuda.memory_reserved()
|
|
# Solo verificamos que no lanza excepcion y que la llamada completo
|
|
assert reserved >= 0, "memory_reserved debe ser >= 0"
|