unsloth/studio/backend/tests/test_diffusion_cache.py

175 lines
6.7 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Hermetic CPU tests for opt-in step caching (First-Block-Cache).
``diffusers`` is stubbed via ``sys.modules`` (the module under test imports
``FirstBlockCacheConfig`` lazily), and the pipeline is a fake that records the engaged config.
So normalisation, the CacheMixin (``enable_cache``) gating, threshold selection, and the
best-effort failure handling are all exercised without torch or a real diffusers model.
"""
from __future__ import annotations
import sys
import types
import pytest
from core.inference.diffusion_cache import (
DEFAULT_FBCACHE_THRESHOLD,
QUANT_FBCACHE_THRESHOLD,
TC_FBCACHE,
apply_step_cache,
normalize_transformer_cache,
)
# ── normalize_transformer_cache ────────────────────────────────────────────────────
def test_normalize_disabled_values_are_none():
for value in (None, "", " ", "none", "off", "OFF", "None"):
assert normalize_transformer_cache(value) is None
def test_normalize_fbcache_and_casing():
assert normalize_transformer_cache("fbcache") == TC_FBCACHE
assert normalize_transformer_cache("FBCache") == TC_FBCACHE
assert normalize_transformer_cache(" fbcache ") == TC_FBCACHE
def test_normalize_rejects_unknown():
with pytest.raises(ValueError):
normalize_transformer_cache("deepcache")
# ── apply_step_cache ───────────────────────────────────────────────────────────────
class _Config:
def __init__(self, threshold):
self.threshold = threshold
class _MixinTransformer:
"""A CacheMixin-style transformer: exposes ``enable_cache``."""
def __init__(self, *, fail = False):
self.fail = fail
self.enabled_with = None
def enable_cache(self, config):
if self.fail:
raise RuntimeError("block signature not recognised")
self.enabled_with = config
class _NonCacheMixinTransformer:
"""A transformer with no ``enable_cache`` (not a CacheMixin) -> must run uncached.
Its pipeline opens no ``cache_context``, so installing FBCache would crash at generation;
the load runs uncached instead (e.g. Z-Image)."""
def _pipe(transformer):
return types.SimpleNamespace(transformer = transformer)
def _stub_diffusers(monkeypatch, *, hook_recorder = None):
diffusers = types.ModuleType("diffusers")
diffusers.FirstBlockCacheConfig = _Config
monkeypatch.setitem(sys.modules, "diffusers", diffusers)
hooks = types.ModuleType("diffusers.hooks")
def _apply_first_block_cache(transformer, config):
if hook_recorder is not None:
hook_recorder["transformer"] = transformer
hook_recorder["config"] = config
hooks.apply_first_block_cache = _apply_first_block_cache
monkeypatch.setitem(sys.modules, "diffusers.hooks", hooks)
def test_disabled_mode_is_noop(monkeypatch):
_stub_diffusers(monkeypatch)
t = _MixinTransformer()
assert apply_step_cache(_pipe(t), mode = None) is None
assert apply_step_cache(_pipe(t), mode = "off") is None
assert t.enabled_with is None
def test_enable_cache_path_default_threshold(monkeypatch):
_stub_diffusers(monkeypatch)
t = _MixinTransformer()
engaged = apply_step_cache(_pipe(t), mode = "fbcache")
assert engaged == TC_FBCACHE
assert t.enabled_with.threshold == DEFAULT_FBCACHE_THRESHOLD
assert t._unsloth_step_cache == f"fbcache@{DEFAULT_FBCACHE_THRESHOLD}"
def test_quant_active_raises_default_threshold(monkeypatch):
_stub_diffusers(monkeypatch)
t = _MixinTransformer()
apply_step_cache(_pipe(t), mode = "fbcache", quant_active = True)
assert t.enabled_with.threshold == QUANT_FBCACHE_THRESHOLD
def test_explicit_threshold_overrides_quant(monkeypatch):
_stub_diffusers(monkeypatch)
t = _MixinTransformer()
apply_step_cache(_pipe(t), mode = "fbcache", threshold = 0.2, quant_active = True)
assert t.enabled_with.threshold == 0.2
def test_non_cachemixin_runs_uncached(monkeypatch):
# A transformer without enable_cache (e.g. Z-Image) must NOT install the standalone hook
# -- its pipeline opens no cache_context, so it runs uncached instead of crashing at gen.
rec: dict = {}
_stub_diffusers(monkeypatch, hook_recorder = rec)
t = _NonCacheMixinTransformer()
assert apply_step_cache(_pipe(t), mode = "fbcache") is None
assert rec == {} # the standalone hook was never called
def test_incompatible_model_runs_uncached(monkeypatch):
# enable_cache raising (e.g. unrecognised block signature) must not fail the load.
_stub_diffusers(monkeypatch)
t = _MixinTransformer(fail = True)
assert apply_step_cache(_pipe(t), mode = "fbcache") is None
def test_enable_cache_failure_rolls_back_partial_hooks(monkeypatch):
# enable_cache can raise after hooking some blocks; the reported-uncached model
# must not actually run half-cached, so the failure path calls disable_cache.
_stub_diffusers(monkeypatch)
t = _MixinTransformer(fail = True)
t.disabled = False
t.disable_cache = lambda: setattr(t, "disabled", True)
assert apply_step_cache(_pipe(t), mode = "fbcache") is None
assert t.disabled is True
def test_config_import_falls_back_to_hooks_module(monkeypatch):
# Older diffusers exports FirstBlockCacheConfig only from diffusers.hooks.
diffusers = types.ModuleType("diffusers") # no FirstBlockCacheConfig attribute
monkeypatch.setitem(sys.modules, "diffusers", diffusers)
hooks = types.ModuleType("diffusers.hooks")
hooks.FirstBlockCacheConfig = _Config
monkeypatch.setitem(sys.modules, "diffusers.hooks", hooks)
t = _MixinTransformer()
assert apply_step_cache(_pipe(t), mode = "fbcache") == TC_FBCACHE
assert t.enabled_with.threshold == DEFAULT_FBCACHE_THRESHOLD
def test_missing_transformer_is_none(monkeypatch):
_stub_diffusers(monkeypatch)
pipe = types.SimpleNamespace(transformer = None)
assert apply_step_cache(pipe, mode = "fbcache") is None
def test_diffusers_unavailable_runs_uncached(monkeypatch):
# no diffusers import -> best-effort returns None, load proceeds uncached. Block the
# hooks module too: the config import falls back to diffusers.hooks, which a REAL
# earlier import in the test session may have left cached in sys.modules.
monkeypatch.setitem(sys.modules, "diffusers", None)
monkeypatch.setitem(sys.modules, "diffusers.hooks", None)
t = _MixinTransformer()
assert apply_step_cache(_pipe(t), mode = "fbcache") is None