chore: auto-commit (95 archivos)
- 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>
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user